Skip to content

Commit c8965c8

Browse files
sampottsclaude
andcommitted
feat(skin): implement default and minimal skins for HTML player
Add video and audio skins with CSS and Tailwind variants, introduce skin-mixin preset pattern, update icon build pipeline, and simplify React skins to share the new skin definitions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4a29580 commit c8965c8

50 files changed

Lines changed: 3521 additions & 2333 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/html/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@
8989
"dependencies": {
9090
"@videojs/core": "workspace:*",
9191
"@videojs/element": "workspace:*",
92+
"@videojs/icons": "workspace:*",
93+
"@videojs/skins": "workspace:*",
9294
"@videojs/store": "workspace:*",
9395
"@videojs/utils": "workspace:*"
9496
},
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.media-minimal-skin {
2+
color: var(--media-color, red);
3+
}
4+
5+
audio-player {
6+
display: contents;
7+
}
8+
9+
audio-minimal-skin {
10+
display: contents;
11+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ReactiveElement } from '@videojs/element';
2+
3+
function getTemplateHTML() {
4+
return /*html*/ `<div></div>`;
5+
}
6+
7+
export class MinimalAudioSkinElement extends ReactiveElement {
8+
static readonly tagName = 'audio-minimal-skin';
9+
static getTemplateHTML = getTemplateHTML;
10+
11+
constructor() {
12+
super();
13+
const children = [...this.childNodes];
14+
this.innerHTML = getTemplateHTML();
15+
const container = this.firstElementChild;
16+
if (container) for (const child of children) container.append(child);
17+
}
18+
}
19+
20+
customElements.define(MinimalAudioSkinElement.tagName, MinimalAudioSkinElement);
21+
22+
declare global {
23+
interface HTMLElementTagNameMap {
24+
[MinimalAudioSkinElement.tagName]: MinimalAudioSkinElement;
25+
}
26+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import "@videojs/skins/audio/default.css";
2+
3+
audio-player {
4+
display: contents;
5+
}
6+
7+
audio-skin {
8+
display: contents;
9+
}
Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,26 @@
1-
// TODO: Implement AudioSkinElement and then register it here
1+
import { ReactiveElement } from '@videojs/element';
2+
3+
function getTemplateHTML() {
4+
return /*html*/ `<div></div>`;
5+
}
6+
7+
export class AudioSkinElement extends ReactiveElement {
8+
static readonly tagName = 'audio-skin';
9+
static getTemplateHTML = getTemplateHTML;
10+
11+
constructor() {
12+
super();
13+
const children = [...this.childNodes];
14+
this.innerHTML = getTemplateHTML();
15+
const container = this.firstElementChild;
16+
if (container) for (const child of children) container.append(child);
17+
}
18+
}
19+
20+
customElements.define(AudioSkinElement.tagName, AudioSkinElement);
21+
22+
declare global {
23+
interface HTMLElementTagNameMap {
24+
[AudioSkinElement.tagName]: AudioSkinElement;
25+
}
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "@videojs/skins/video/minimal.css";
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { ReactiveElement } from '@videojs/element';
2+
import {
3+
bufferingIndicator,
4+
button,
5+
buttonGroup,
6+
controls,
7+
error,
8+
icon,
9+
iconContainer,
10+
iconFlipped,
11+
iconState,
12+
overlay,
13+
popup,
14+
root,
15+
seek,
16+
slider,
17+
time,
18+
} from '@videojs/skins/video/minimal.tailwind';
19+
import { cn } from '@videojs/utils/style';
20+
import { SkinMixin } from '../../presets/skin-mixin';
21+
22+
// Side-effect imports: register all custom elements used in the template.
23+
import '@videojs/icons/element';
24+
import '../media/container';
25+
import '../ui/buffering-indicator';
26+
import '../ui/controls';
27+
import '../ui/fullscreen-button';
28+
import '../ui/mute-button';
29+
import '../ui/pip-button';
30+
import '../ui/play-button';
31+
import '../ui/playback-rate-button';
32+
import '../ui/popover';
33+
import '../ui/seek-button';
34+
import '../ui/time';
35+
import '../ui/time-slider';
36+
import '../ui/volume-slider';
37+
import { playbackRate } from '@videojs/skins/video/default.tailwind';
38+
39+
const SEEK_TIME = 10;
40+
41+
function getTemplateHTML() {
42+
return /*html*/ `
43+
<media-container class="${root}">
44+
<media-buffering-indicator class="${bufferingIndicator}">
45+
<media-icon family="minimal" name="spinner"></media-icon>
46+
</media-buffering-indicator>
47+
48+
<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
49+
<div class="${error.dialog}">
50+
<div class="${error.content}">
51+
<p id="media-error-title" class="${error.title}">Something went wrong.</p>
52+
<p id="media-error-description">An error occurred while trying to play the video. Please try again.</p>
53+
</div>
54+
<div class="${error.actions}">
55+
<button type="button" class="${cn(button.base, button.default)}">OK</button>
56+
</div>
57+
</div>
58+
</div>
59+
60+
<media-controls data-controls="" class="${controls}">
61+
<span class="${buttonGroup}">
62+
<media-play-button class="${cn(button.base, button.icon, iconState.play.button)}">
63+
<media-icon family="minimal" name="restart" class="${cn(icon, iconState.play.restart)}"></media-icon>
64+
<media-icon family="minimal" name="play" class="${cn(icon, iconState.play.play)}"></media-icon>
65+
<media-icon family="minimal" name="pause" class="${cn(icon, iconState.play.pause)}"></media-icon>
66+
</media-play-button>
67+
68+
<media-seek-button seconds="${-SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
69+
<span class="${iconContainer}">
70+
<media-icon family="minimal" name="seek" class="${cn(icon, iconFlipped)}"></media-icon>
71+
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
72+
</span>
73+
</media-seek-button>
74+
75+
<media-seek-button seconds="${SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
76+
<span class="${iconContainer}">
77+
<media-icon family="minimal" name="seek" class="${icon}"></media-icon>
78+
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
79+
</span>
80+
</media-seek-button>
81+
</span>
82+
83+
<span class="${time.controls}">
84+
<media-time-group class="${time.group}">
85+
<media-time type="current" class="${time.current}"></media-time>
86+
<media-time-separator class="${time.separator}"></media-time-separator>
87+
<media-time type="duration" class="${time.duration}"></media-time>
88+
</media-time-group>
89+
90+
<media-time-slider class="${slider.root}">
91+
<media-slider-track class="${slider.track}">
92+
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
93+
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
94+
</media-slider-track>
95+
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
96+
</media-time-slider>
97+
</span>
98+
99+
<span class="${buttonGroup}">
100+
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
101+
</media-playback-rate-button>
102+
103+
<media-mute-button commandfor="volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
104+
<media-icon family="minimal" name="volume-off" class="${cn(icon, iconState.mute.volumeOff)}"></media-icon>
105+
<media-icon family="minimal" name="volume-low" class="${cn(icon, iconState.mute.volumeLow)}"></media-icon>
106+
<media-icon family="minimal" name="volume-high" class="${cn(icon, iconState.mute.volumeHigh)}"></media-icon>
107+
</media-mute-button>
108+
109+
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
110+
<media-volume-slider class="${slider.root}" orientation="vertical" thumb-alignment="edge">
111+
<media-slider-track class="${slider.track}">
112+
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
113+
</media-slider-track>
114+
<media-slider-thumb class="${slider.thumb.base}"></media-slider-thumb>
115+
</media-volume-slider>
116+
</media-popover>
117+
118+
<!--<button type="button" class="${cn(button.base, button.icon)}" aria-label="Captions">
119+
<media-icon family="minimal" name="captions-off" class="${icon}"></media-icon>
120+
<media-icon family="minimal" name="captions-on" class="${icon}"></media-icon>
121+
</button>-->
122+
123+
<media-pip-button class="${cn(button.base, button.icon)}">
124+
<media-icon family="minimal" name="pip" class="${icon}"></media-icon>
125+
</media-pip-button>
126+
127+
<media-fullscreen-button class="${cn(button.base, button.icon, iconState.fullscreen.button)}">
128+
<media-icon family="minimal" name="fullscreen-enter" class="${cn(icon, iconState.fullscreen.enter)}"></media-icon>
129+
<media-icon family="minimal" name="fullscreen-exit" class="${cn(icon, iconState.fullscreen.exit)}"></media-icon>
130+
</media-fullscreen-button>
131+
</span>
132+
</media-controls>
133+
134+
<div class="${overlay}"></div>
135+
</media-container>
136+
`;
137+
}
138+
139+
export class MinimalVideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
140+
static readonly tagName = 'video-minimal-skin';
141+
static getTemplateHTML = getTemplateHTML;
142+
}
143+
144+
customElements.define(MinimalVideoSkinTailwindElement.tagName, MinimalVideoSkinTailwindElement);
145+
146+
declare global {
147+
interface HTMLElementTagNameMap {
148+
[MinimalVideoSkinTailwindElement.tagName]: MinimalVideoSkinTailwindElement;
149+
}
150+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { ReactiveElement } from '@videojs/element';
2+
import { SkinMixin } from '../../presets/skin-mixin';
3+
4+
// Side-effect imports: register all custom elements used in the template.
5+
import '@videojs/icons/element';
6+
import '../media/container';
7+
import '../ui/buffering-indicator';
8+
import '../ui/controls';
9+
import '../ui/fullscreen-button';
10+
import '../ui/mute-button';
11+
import '../ui/pip-button';
12+
import '../ui/play-button';
13+
import '../ui/playback-rate-button';
14+
import '../ui/popover';
15+
import '../ui/seek-button';
16+
import '../ui/time';
17+
import '../ui/time-slider';
18+
import '../ui/volume-slider';
19+
20+
const SEEK_TIME = 10;
21+
22+
function getTemplateHTML() {
23+
return /*html*/ `
24+
<media-container class="media-minimal-skin">
25+
<media-buffering-indicator class="media-buffering-indicator">
26+
<media-icon family="minimal" name="spinner" class="media-icon"></media-icon>
27+
</media-buffering-indicator>
28+
29+
<div class="media-error" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
30+
<div class="media-error__dialog">
31+
<div class="media-error__content">
32+
<p id="media-error-title" class="media-error__title">Something went wrong.</p>
33+
<p id="media-error-description" class="media-error__description">An error occurred while trying to play the video. Please try again.</p>
34+
</div>
35+
<div class="media-error__actions">
36+
<button type="button" class="media-button">OK</button>
37+
</div>
38+
</div>
39+
</div>
40+
41+
<media-controls class="media-controls">
42+
<span class="media-button-group">
43+
<media-play-button class="media-button media-button--icon media-button--play">
44+
<media-icon family="minimal" name="restart" class="media-icon media-icon--restart"></media-icon>
45+
<media-icon family="minimal" name="play" class="media-icon media-icon--play"></media-icon>
46+
<media-icon family="minimal" name="pause" class="media-icon media-icon--pause"></media-icon>
47+
</media-play-button>
48+
49+
<media-seek-button seconds="${-SEEK_TIME}" class="media-button media-button--icon media-button--seek">
50+
<span class="media-icon__container">
51+
<media-icon family="minimal" name="seek" class="media-icon media-icon--flipped"></media-icon>
52+
<span class="media-icon__label">${SEEK_TIME}</span>
53+
</span>
54+
</media-seek-button>
55+
56+
<media-seek-button seconds="${SEEK_TIME}" class="media-button media-button--icon media-button--seek">
57+
<span class="media-icon__container">
58+
<media-icon family="minimal" name="seek" class="media-icon"></media-icon>
59+
<span class="media-icon__label">${SEEK_TIME}</span>
60+
</span>
61+
</media-seek-button>
62+
</span>
63+
64+
<span class="media-time-controls">
65+
<media-time-group class="media-time">
66+
<media-time type="current" class="media-time__value media-time__value--current"></media-time>
67+
<media-time-separator class="media-time__separator"></media-time-separator>
68+
<media-time type="duration" class="media-time__value media-time__value--duration"></media-time>
69+
</media-time-group>
70+
71+
<media-time-slider class="media-slider">
72+
<media-slider-track class="media-slider__track">
73+
<media-slider-fill class="media-slider__fill"></media-slider-fill>
74+
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
75+
</media-slider-track>
76+
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
77+
</media-time-slider>
78+
</span>
79+
80+
<span class="media-button-group">
81+
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
82+
</media-playback-rate-button>
83+
84+
<media-mute-button commandfor="volume-popover" class="media-button media-button--icon media-button--mute">
85+
<media-icon family="minimal" name="volume-off" class="media-icon media-icon--volume-off"></media-icon>
86+
<media-icon family="minimal" name="volume-low" class="media-icon media-icon--volume-low"></media-icon>
87+
<media-icon family="minimal" name="volume-high" class="media-icon media-icon--volume-high"></media-icon>
88+
</media-mute-button>
89+
90+
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popup media-popup--volume media-popup-animation">
91+
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
92+
<media-slider-track class="media-slider__track">
93+
<media-slider-fill class="media-slider__fill"></media-slider-fill>
94+
</media-slider-track>
95+
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
96+
</media-volume-slider>
97+
</media-popover>
98+
99+
<!-- <button type="button" class="media-button media-button--icon media-button--captions" aria-label="Captions">
100+
<media-icon family="minimal" name="captions-off" class="media-icon media-icon--captions-off"></media-icon>
101+
<media-icon family="minimal" name="captions-on" class="media-icon media-icon--captions-on"></media-icon>
102+
</button> -->
103+
104+
<media-pip-button class="media-button media-button--icon">
105+
<media-icon family="minimal" name="pip" class="media-icon"></media-icon>
106+
</media-pip-button>
107+
108+
<media-fullscreen-button class="media-button media-button--icon media-button--fullscreen">
109+
<media-icon family="minimal" name="fullscreen-enter" class="media-icon media-icon--fullscreen-enter"></media-icon>
110+
<media-icon family="minimal" name="fullscreen-exit" class="media-icon media-icon--fullscreen-exit"></media-icon>
111+
</media-fullscreen-button>
112+
</span>
113+
</media-controls>
114+
115+
<div class="media-overlay"></div>
116+
</media-container>
117+
`;
118+
}
119+
120+
export class MinimalVideoSkinElement extends SkinMixin(ReactiveElement) {
121+
static readonly tagName = 'video-minimal-skin';
122+
static getTemplateHTML = getTemplateHTML;
123+
}
124+
125+
customElements.define(MinimalVideoSkinElement.tagName, MinimalVideoSkinElement);
126+
127+
declare global {
128+
interface HTMLElementTagNameMap {
129+
[MinimalVideoSkinElement.tagName]: MinimalVideoSkinElement;
130+
}
131+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "@videojs/skins/video/default.css";

0 commit comments

Comments
 (0)