feat(core): add overlay primitive

This commit is contained in:
Rahim
2026-06-22 20:19:50 -07:00
parent aefa6c39c8
commit 89f13eb6d7
39 changed files with 476 additions and 32 deletions
+2
View File
@@ -54,6 +54,8 @@ export * from './ui/menu/props';
export * from './ui/mute-button/mute-button-core';
export * from './ui/mute-button/mute-button-data-attrs';
export * from './ui/mute-button/props';
export * from './ui/overlay/overlay-core';
export * from './ui/overlay/overlay-data-attrs';
export * from './ui/pip-button/pip-button-core';
export * from './ui/pip-button/pip-button-data-attrs';
export * from './ui/pip-button/props';
@@ -16,6 +16,7 @@ import HotkeyDef from './hotkey/hotkey-component';
import LiveButtonDef from './live-button/live-button-component';
import MenuDef from './menu/menu-component';
import MuteButtonDef from './mute-button/mute-button-component';
import OverlayDef from './overlay/overlay-component';
import PiPButtonDef from './pip-button/pip-button-component';
import PlayButtonDef from './play-button/play-button-component';
import PlaybackRateButtonDef from './playback-rate-button/playback-rate-button-component';
@@ -50,6 +51,7 @@ export const Hotkey = createComponent(HotkeyDef);
export const LiveButton = createComponent(LiveButtonDef);
export const Menu = createComponent(MenuDef);
export const MuteButton = createComponent(MuteButtonDef);
export const Overlay = createComponent(OverlayDef);
export const PiPButton = createComponent(PiPButtonDef);
export const PlaybackRateButton = createComponent(PlaybackRateButtonDef);
export const PlaybackRateRadioGroup = createComponent(PlaybackRateRadioGroupDef);
@@ -85,6 +87,7 @@ export const COMPONENTS = {
LiveButton: LiveButtonDef,
Menu: MenuDef,
MuteButton: MuteButtonDef,
Overlay: OverlayDef,
PiPButton: PiPButtonDef,
PlaybackRateButton: PlaybackRateButtonDef,
PlaybackRateRadioGroup: PlaybackRateRadioGroupDef,
@@ -0,0 +1,7 @@
import { defineComponent } from '../manifest';
import { OverlayDataAttrs } from './overlay-data-attrs';
export default defineComponent()({
name: 'Overlay',
dataAttrs: OverlayDataAttrs,
});
@@ -0,0 +1,36 @@
import type { MediaControlsState, MediaErrorState } from '../../media/state';
export interface OverlayMediaState {
controls?: MediaControlsState | null | undefined;
error?: MediaErrorState | null | undefined;
}
export interface OverlayState {
visible: boolean;
controlsVisible: boolean;
errorVisible: boolean;
}
export class OverlayCore {
#media: OverlayMediaState = {};
setMedia(media: OverlayMediaState): void {
this.#media = media;
}
getState(): OverlayState {
const controlsVisible = this.#media.controls?.controlsVisible ?? false;
const errorVisible = Boolean(this.#media.error?.error);
return {
visible: controlsVisible || errorVisible,
controlsVisible,
errorVisible,
};
}
}
export namespace OverlayCore {
export type MediaState = OverlayMediaState;
export type State = OverlayState;
}
@@ -0,0 +1,11 @@
import type { StateAttrMap } from '../types';
import type { OverlayState } from './overlay-core';
export const OverlayDataAttrs = {
/** Present when the overlay should be visible. */
visible: 'data-visible',
/** Present when controls visibility is showing the overlay. */
controlsVisible: 'data-controls-visible',
/** Present when error visibility is showing the overlay. */
errorVisible: 'data-error-visible',
} as const satisfies StateAttrMap<OverlayState>;
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import type { MediaControlsState, MediaErrorState } from '../../../media/state';
import { OverlayCore } from '../overlay-core';
describe('OverlayCore', () => {
describe('getState', () => {
it('is visible when controls are visible', () => {
const core = new OverlayCore();
core.setMedia({ controls: createControlsState({ controlsVisible: true }), error: createErrorState() });
expect(core.getState()).toEqual({
visible: true,
controlsVisible: true,
errorVisible: false,
});
});
it('is visible when an error is active', () => {
const core = new OverlayCore();
core.setMedia({
controls: createControlsState({ controlsVisible: false }),
error: createErrorState({ code: 1, message: 'failed' }),
});
expect(core.getState()).toEqual({
visible: true,
controlsVisible: false,
errorVisible: true,
});
});
it('is hidden when neither controls nor error need it', () => {
const core = new OverlayCore();
core.setMedia({ controls: createControlsState({ controlsVisible: false }), error: createErrorState() });
expect(core.getState()).toEqual({
visible: false,
controlsVisible: false,
errorVisible: false,
});
});
it('treats missing feature state as hidden', () => {
const core = new OverlayCore();
core.setMedia({});
expect(core.getState()).toEqual({
visible: false,
controlsVisible: false,
errorVisible: false,
});
});
});
});
function createControlsState(overrides: Partial<MediaControlsState> = {}): MediaControlsState {
return {
userActive: true,
controlsVisible: true,
toggleControls: () => true,
...overrides,
};
}
function createErrorState(error: MediaErrorState['error'] = null): MediaErrorState {
return {
error,
dismissError: () => {},
};
}