feat(skin): implement default and minimal skins for HTML player (#698)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Potts
2026-03-04 22:35:45 +11:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9a5dfab4e8
commit c5cafae57f
51 changed files with 3513 additions and 2334 deletions
@@ -0,0 +1,11 @@
.media-minimal-skin {
color: var(--media-color, red);
}
audio-player {
display: contents;
}
audio-minimal-skin {
display: contents;
}
@@ -0,0 +1,26 @@
import { ReactiveElement } from '@videojs/element';
function getTemplateHTML() {
return /*html*/ `<div></div>`;
}
export class MinimalAudioSkinElement extends ReactiveElement {
static readonly tagName = 'audio-minimal-skin';
static getTemplateHTML = getTemplateHTML;
constructor() {
super();
const children = [...this.childNodes];
this.innerHTML = getTemplateHTML();
const container = this.firstElementChild;
if (container) for (const child of children) container.append(child);
}
}
customElements.define(MinimalAudioSkinElement.tagName, MinimalAudioSkinElement);
declare global {
interface HTMLElementTagNameMap {
[MinimalAudioSkinElement.tagName]: MinimalAudioSkinElement;
}
}
+9
View File
@@ -0,0 +1,9 @@
@import "@videojs/skins/audio/default.css";
audio-player {
display: contents;
}
audio-skin {
display: contents;
}
+26 -1
View File
@@ -1 +1,26 @@
// TODO: Implement AudioSkinElement and then register it here
import { ReactiveElement } from '@videojs/element';
function getTemplateHTML() {
return /*html*/ `<div></div>`;
}
export class AudioSkinElement extends ReactiveElement {
static readonly tagName = 'audio-skin';
static getTemplateHTML = getTemplateHTML;
constructor() {
super();
const children = [...this.childNodes];
this.innerHTML = getTemplateHTML();
const container = this.firstElementChild;
if (container) for (const child of children) container.append(child);
}
}
customElements.define(AudioSkinElement.tagName, AudioSkinElement);
declare global {
interface HTMLElementTagNameMap {
[AudioSkinElement.tagName]: AudioSkinElement;
}
}
+46
View File
@@ -0,0 +1,46 @@
import type { ReactiveElement } from '@videojs/element';
import type { Constructor } from '@videojs/utils/types';
/**
* Mixin for skin elements that renders the template from a static
* `getTemplateHTML` method and resolves `<slot name="media">` placeholders
* by replacing them with the actual media element children.
*/
export function SkinMixin<Base extends Constructor<ReactiveElement>>(BaseClass: Base): Base {
class SkinElement extends (BaseClass as Constructor<ReactiveElement>) {
constructor(...args: any[]) {
super(...args);
const ctor = this.constructor as { getTemplateHTML?: () => string };
if (ctor.getTemplateHTML) {
const children = [...this.childNodes];
this.innerHTML = ctor.getTemplateHTML();
this.#resolveSlots(children);
}
}
override connectedCallback(): void {
// During innerHTML parsing, children aren't available in the constructor.
// Resolve any remaining slotted elements before the container connects.
this.#resolveSlots();
super.connectedCallback();
}
#resolveSlots(nodes?: ChildNode[]): void {
const slot = this.querySelector('slot[name="media"]');
if (!slot) return;
// Collect media from either the provided node list (constructor) or
// from direct children that haven't been placed yet (connectedCallback).
const media = nodes
? nodes.filter((n): n is HTMLElement => n instanceof HTMLElement && n.getAttribute('slot') === 'media')
: [...this.querySelectorAll<HTMLElement>(':scope > [slot="media"]')];
for (const el of media) slot.before(el);
slot.remove();
}
}
return SkinElement as unknown as Base;
}
@@ -0,0 +1 @@
@import "@videojs/skins/video/minimal.css";
@@ -0,0 +1,152 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render/minimal';
import {
bufferingIndicator,
button,
buttonGroup,
controls,
error,
icon,
iconContainer,
iconFlipped,
iconState,
overlay,
popup,
root,
seek,
slider,
time,
} from '@videojs/skins/video/minimal.tailwind';
import { cn } from '@videojs/utils/style';
import { SkinMixin } from '../skin-mixin';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
import '../ui/buffering-indicator';
import '../ui/controls';
import '../ui/fullscreen-button';
import '../ui/mute-button';
import '../ui/pip-button';
import '../ui/play-button';
import '../ui/playback-rate-button';
import '../ui/popover';
import '../ui/seek-button';
import '../ui/time';
import '../ui/time-slider';
import '../ui/volume-slider';
import { playbackRate } from '@videojs/skins/video/default.tailwind';
const SEEK_TIME = 10;
function getTemplateHTML() {
return /*html*/ `
<media-container class="${root}">
<media-buffering-indicator class="${bufferingIndicator}">
${renderIcon('spinner')}
</media-buffering-indicator>
<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
<div class="${error.dialog}">
<div class="${error.content}">
<p id="media-error-title" class="${error.title}">Something went wrong.</p>
<p id="media-error-description">An error occurred while trying to play the video. Please try again.</p>
</div>
<div class="${error.actions}">
<button type="button" class="${cn(button.base, button.default)}">OK</button>
</div>
</div>
</div>
<media-controls data-controls="" class="${controls}">
<span class="${buttonGroup}">
<media-play-button class="${cn(button.base, button.icon, iconState.play.button)}">
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
</media-play-button>
<media-seek-button seconds="${-SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
<span class="${iconContainer}">
${renderIcon('seek', { class: cn(icon, iconFlipped) })}
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
</span>
</media-seek-button>
<media-seek-button seconds="${SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
<span class="${iconContainer}">
${renderIcon('seek', { class: icon })}
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
</span>
</media-seek-button>
</span>
<span class="${time.controls}">
<media-time-group class="${time.group}">
<media-time type="current" class="${time.current}"></media-time>
<media-time-separator class="${time.separator}"></media-time-separator>
<media-time type="duration" class="${time.duration}"></media-time>
</media-time-group>
<media-time-slider class="${slider.root}">
<media-slider-track class="${slider.track}">
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
</media-time-slider>
</span>
<span class="${buttonGroup}">
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
</media-playback-rate-button>
<media-mute-button commandfor="volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
${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) })}
</media-mute-button>
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
<media-volume-slider class="${slider.root}" orientation="vertical" thumb-alignment="edge">
<media-slider-track class="${slider.track}">
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="${slider.thumb.base}"></media-slider-thumb>
</media-volume-slider>
</media-popover>
<!--<button type="button" class="${cn(button.base, button.icon)}" aria-label="Captions">
${renderIcon('captions-off', { class: icon })}
${renderIcon('captions-on', { class: icon })}
</button>-->
<media-pip-button class="${cn(button.base, button.icon)}">
${renderIcon('pip', { class: icon })}
</media-pip-button>
<media-fullscreen-button class="${cn(button.base, button.icon, iconState.fullscreen.button)}">
${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })}
${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })}
</media-fullscreen-button>
</span>
</media-controls>
<div class="${overlay}"></div>
<slot name="media"></slot>
</media-container>
`;
}
export class MinimalVideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
static readonly tagName = 'video-minimal-skin-tailwind';
static getTemplateHTML = getTemplateHTML;
}
customElements.define(MinimalVideoSkinTailwindElement.tagName, MinimalVideoSkinTailwindElement);
declare global {
interface HTMLElementTagNameMap {
[MinimalVideoSkinTailwindElement.tagName]: MinimalVideoSkinTailwindElement;
}
}
@@ -0,0 +1,133 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render/minimal';
import { SkinMixin } from '../skin-mixin';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
import '../ui/buffering-indicator';
import '../ui/controls';
import '../ui/fullscreen-button';
import '../ui/mute-button';
import '../ui/pip-button';
import '../ui/play-button';
import '../ui/playback-rate-button';
import '../ui/popover';
import '../ui/seek-button';
import '../ui/time';
import '../ui/time-slider';
import '../ui/volume-slider';
const SEEK_TIME = 10;
function getTemplateHTML() {
return /*html*/ `
<media-container class="media-minimal-skin">
<media-buffering-indicator class="media-buffering-indicator">
${renderIcon('spinner', { class: 'media-icon' })}
</media-buffering-indicator>
<div class="media-error" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
<div class="media-error__dialog">
<div class="media-error__content">
<p id="media-error-title" class="media-error__title">Something went wrong.</p>
<p id="media-error-description" class="media-error__description">An error occurred while trying to play the video. Please try again.</p>
</div>
<div class="media-error__actions">
<button type="button" class="media-button">OK</button>
</div>
</div>
</div>
<media-controls class="media-controls">
<span class="media-button-group">
<media-play-button class="media-button media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-seek-button seconds="${-SEEK_TIME}" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
${renderIcon('seek', { class: 'media-icon media-icon--flipped' })}
<span class="media-icon__label">${SEEK_TIME}</span>
</span>
</media-seek-button>
<media-seek-button seconds="${SEEK_TIME}" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
${renderIcon('seek', { class: 'media-icon' })}
<span class="media-icon__label">${SEEK_TIME}</span>
</span>
</media-seek-button>
</span>
<span class="media-time-controls">
<media-time-group class="media-time">
<media-time type="current" class="media-time__value media-time__value--current"></media-time>
<media-time-separator class="media-time__separator"></media-time-separator>
<media-time type="duration" class="media-time__value media-time__value--duration"></media-time>
</media-time-group>
<media-time-slider class="media-slider">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
</media-time-slider>
</span>
<span class="media-button-group">
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
</media-playback-rate-button>
<media-mute-button commandfor="volume-popover" class="media-button media-button--icon media-button--mute">
${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' })}
</media-mute-button>
<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">
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
</media-volume-slider>
</media-popover>
<!-- <button type="button" class="media-button media-button--icon media-button--captions" aria-label="Captions">
${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })}
${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })}
</button> -->
<media-pip-button class="media-button media-button--icon">
${renderIcon('pip', { class: 'media-icon' })}
</media-pip-button>
<media-fullscreen-button class="media-button media-button--icon media-button--fullscreen">
${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })}
${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })}
</media-fullscreen-button>
</span>
</media-controls>
<div class="media-overlay"></div>
<slot name="media"></slot>
</media-container>
`;
}
export class MinimalVideoSkinElement extends SkinMixin(ReactiveElement) {
static readonly tagName = 'video-minimal-skin';
static getTemplateHTML = getTemplateHTML;
}
customElements.define(MinimalVideoSkinElement.tagName, MinimalVideoSkinElement);
declare global {
interface HTMLElementTagNameMap {
[MinimalVideoSkinElement.tagName]: MinimalVideoSkinElement;
}
}
+1
View File
@@ -0,0 +1 @@
@import "@videojs/skins/video/default.css";
@@ -0,0 +1,145 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render';
import {
bufferingIndicator,
button,
controls,
error,
icon,
iconContainer,
iconFlipped,
iconState,
overlay,
playbackRate,
popup,
root,
seek,
slider,
time,
} from '@videojs/skins/video/default.tailwind';
import { cn } from '@videojs/utils/style';
import { SkinMixin } from '../skin-mixin';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
import '../ui/buffering-indicator';
import '../ui/controls';
import '../ui/fullscreen-button';
import '../ui/mute-button';
import '../ui/pip-button';
import '../ui/play-button';
import '../ui/playback-rate-button';
import '../ui/popover';
import '../ui/seek-button';
import '../ui/time';
import '../ui/time-slider';
import '../ui/volume-slider';
const SEEK_TIME = 10;
function getTemplateHTML() {
return /*html*/ `
<media-container class="${root}">
<media-buffering-indicator class="${bufferingIndicator.root}">
<div class="${bufferingIndicator.container}">
${renderIcon('spinner')}
</div>
</media-buffering-indicator>
<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
<div class="${error.dialog}">
<div class="${error.content}">
<p id="media-error-title" class="${error.title}">Something went wrong.</p>
<p id="media-error-description" class="${error.description}">An error occurred while trying to play the video. Please try again.</p>
</div>
<div class="${error.actions}">
<button type="button" class="${cn(button.base, button.default)}">OK</button>
</div>
</div>
</div>
<media-controls data-controls="" class="${controls}">
<media-play-button class="${cn(button.base, button.icon, iconState.play.button)}">
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
</media-play-button>
<media-seek-button seconds="${-SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
<span class="${iconContainer}">
${renderIcon('seek', { class: cn(icon, iconFlipped) })}
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
</span>
</media-seek-button>
<media-seek-button seconds="${SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
<span class="${iconContainer}">
${renderIcon('seek', { class: icon })}
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
</span>
</media-seek-button>
<media-time-group class="${time.group}">
<media-time type="current" class="${time.current}"></media-time>
<media-time-slider class="${slider.root}">
<media-slider-track class="${slider.track}">
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
</media-time-slider>
<media-time type="duration" class="${time.duration}"></media-time>
</media-time-group>
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
</media-playback-rate-button>
<media-mute-button commandfor="volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
${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) })}
</media-mute-button>
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
<media-volume-slider class="${slider.root}" orientation="vertical" thumb-alignment="edge">
<media-slider-track class="${slider.track}">
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.persistent)}"></media-slider-thumb>
</media-volume-slider>
</media-popover>
<!--<button type="button" class="${cn(button.base, button.icon)}" aria-label="Captions">
${renderIcon('captions-off', { class: icon })}
${renderIcon('captions-on', { class: icon })}
</button>-->
<media-pip-button class="${cn(button.base, button.icon)}">
${renderIcon('pip', { class: icon })}
</media-pip-button>
<media-fullscreen-button class="${cn(button.base, button.icon, iconState.fullscreen.button)}">
${renderIcon('fullscreen-enter', { class: cn(icon, iconState.fullscreen.enter) })}
${renderIcon('fullscreen-exit', { class: cn(icon, iconState.fullscreen.exit) })}
</media-fullscreen-button>
</media-controls>
<div class="${overlay}"></div>
<slot name="media"></slot>
</media-container>
`;
}
export class VideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
static readonly tagName = 'video-skin-tailwind';
static getTemplateHTML = getTemplateHTML;
}
customElements.define(VideoSkinTailwindElement.tagName, VideoSkinTailwindElement);
declare global {
interface HTMLElementTagNameMap {
[VideoSkinTailwindElement.tagName]: VideoSkinTailwindElement;
}
}
+127 -1
View File
@@ -1 +1,127 @@
// TODO: Implement VideoSkinElement and then register it here
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render';
import { SkinMixin } from '../skin-mixin';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
import '../ui/buffering-indicator';
import '../ui/controls';
import '../ui/fullscreen-button';
import '../ui/mute-button';
import '../ui/pip-button';
import '../ui/play-button';
import '../ui/playback-rate-button';
import '../ui/popover';
import '../ui/seek-button';
import '../ui/time';
import '../ui/time-slider';
import '../ui/volume-slider';
const SEEK_TIME = 10;
function getTemplateHTML() {
return /*html*/ `
<media-container class="media-default-skin">
<media-buffering-indicator class="media-buffering-indicator">
<div class="media-surface">
${renderIcon('spinner', { class: 'media-icon' })}
</div>
</media-buffering-indicator>
<!--<div class="media-error" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
<div class="media-error__dialog media-surface">
<div class="media-error__content">
<p id="media-error-title" class="media-error__title">Something went wrong.</p>
<p id="media-error-description" class="media-error__description">An error occurred while trying to play the video. Please try again.</p>
</div>
<div class="media-error__actions">
<button type="button" class="media-button">OK</button>
</div>
</div>
</div>-->
<media-controls class="media-surface media-controls">
<media-play-button class="media-button media-button--icon media-button--play">
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
${renderIcon('play', { class: 'media-icon media-icon--play' })}
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
</media-play-button>
<media-seek-button seconds="${-SEEK_TIME}" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
${renderIcon('seek', { class: 'media-icon media-icon--flipped' })}
<span class="media-icon__label">${SEEK_TIME}</span>
</span>
</media-seek-button>
<media-seek-button seconds="${SEEK_TIME}" class="media-button media-button--icon media-button--seek">
<span class="media-icon__container">
${renderIcon('seek', { class: 'media-icon' })}
<span class="media-icon__label">${SEEK_TIME}</span>
</span>
</media-seek-button>
<media-time-group class="media-time">
<media-time type="current" class="media-time__value"></media-time>
<media-time-slider class="media-slider">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
</media-time-slider>
<media-time type="duration" class="media-time__value"></media-time>
</media-time-group>
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
</media-playback-rate-button>
<media-mute-button commandfor="volume-popover" class="media-button media-button--icon media-button--mute">
${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' })}
</media-mute-button>
<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">
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
<media-slider-track class="media-slider__track">
<media-slider-fill class="media-slider__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
</media-volume-slider>
</media-popover>
<!--<button type="button" class="media-button media-button--icon media-button--captions" aria-label="Captions">
${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })}
${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })}
</button>-->
<media-pip-button class="media-button media-button--icon">
${renderIcon('pip', { class: 'media-icon' })}
</media-pip-button>
<media-fullscreen-button class="media-button media-button--icon media-button--fullscreen">
${renderIcon('fullscreen-enter', { class: 'media-icon media-icon--fullscreen-enter' })}
${renderIcon('fullscreen-exit', { class: 'media-icon media-icon--fullscreen-exit' })}
</media-fullscreen-button>
</media-controls>
<div class="media-overlay"></div>
<slot name="media"></slot>
</media-container>
`;
}
export class VideoSkinElement extends SkinMixin(ReactiveElement) {
static readonly tagName = 'video-skin';
static getTemplateHTML = getTemplateHTML;
}
customElements.define(VideoSkinElement.tagName, VideoSkinElement);
declare global {
interface HTMLElementTagNameMap {
[VideoSkinElement.tagName]: VideoSkinElement;
}
}