feat(compiler): improve skin transform dx

This commit is contained in:
Rahim
2026-06-22 16:36:39 -07:00
parent fcee71e1eb
commit aefa6c39c8
10 changed files with 793 additions and 178 deletions
@@ -22,16 +22,65 @@ describe('skins compiler config', () => {
expect(code).toContain('from "@videojs/utils/style"');
expect(code).toContain('from "@/player/context"');
expect(code).toContain('from "@/ui/airplay-button"');
expect(code).toContain('from "@/ui/buffering-indicator"');
expect(code).toContain('from "@/ui/error-dialog"');
expect(code).toContain('from "@/ui/pip-button"');
expect(code).toContain('from "@/ui/play-button"');
expect(code).toContain('from "@/ui/poster"');
expect(code).not.toContain('from "@/ui/slider"');
expect(code).toContain('from "@/utils/use-render"');
expect(code).toContain('from "@videojs/utils/predicate"');
expect(code).toContain('from "@/icons"');
expect(code).not.toContain('from "../skin-renderers"');
expect(code).toContain('from "../types"');
expect(code).toContain('import type { ReactNode } from "react"');
const compactCode = compact(code);
expect(compactCode).toContain(compact('export interface DefaultVideoSkinProps extends BaseVideoSkinProps'));
expect(compactCode).toContain(compact('const iconButton = [button.base, button.subtle, button.icon];'));
expect(compactCode).toContain(compact('children?: ReactNode | undefined;'));
expect(compactCode).toContain(compact('<Container className={cn(container, className)}>'));
expect(compactCode).toContain(
compact('DefaultVideoSkin({ className, children, poster, ...rest }: DefaultVideoSkinProps)')
);
expect(compactCode).toContain(compact('<Container className={cn(container, className)} {...rest}>'));
expect(compactCode).toContain(
compact(
'{poster && <Poster src={isString(poster) ? poster : undefined} render={isRenderProp(poster) ? poster : undefined} />}'
)
);
expect(compactCode).toContain(compact('<BufferingIndicator className={bufferingIndicator.root}>'));
expect(compactCode).toContain(compact('<SpinnerIcon className={icon} />'));
expect(compactCode).not.toContain('BufferingIndicatorRoot');
expect(compactCode).toContain(compact('<ErrorDialog.Popup className={error.popup}>'));
expect(compactCode).toContain(
compact('<ErrorDialog.Title className={error.title}>Something went wrong.</ErrorDialog.Title>')
);
expect(compactCode).toContain(compact('<ErrorDialog.Description className={error.description} />'));
expect(compactCode).toContain(
compact('<ErrorDialog.Close className={cn(button.base, button.primary, error.close)}>OK</ErrorDialog.Close>')
);
expect(compactCode).not.toContain('ErrorDialog.Panel');
expect(compactCode).not.toContain('ErrorDialog.Content');
expect(compactCode).not.toContain('ErrorDialog.Actions');
expect(compactCode).toContain(compact('<Controls.Root className={controls} data-controls="">'));
expect(compactCode).toContain(compact('<Tooltip.Trigger render={<PlayButton'));
expect(compactCode).toContain(compact('<PlayButton className={cn(iconButton, playIcon.button)} type="button">'));
expect(compactCode).toContain(
compact('<SeekButton seconds={-SEEK_TIME} className={cn(iconButton)} type="button">')
);
expect(compactCode).not.toContain(compact('render={<Button />}'));
expect(compactCode).toContain(compact('<TimeSlider.Root className={slider.root}>'));
expect(compactCode).toContain(compact('<TimeSlider.Track className={slider.track}>'));
expect(compactCode).toContain(compact('<TimeSlider.Fill className={cn(slider.fill.base, slider.fill.fill)} />'));
expect(compactCode).toContain(
compact('<TimeSlider.Buffer className={cn(slider.fill.base, slider.fill.buffer)} />')
);
expect(compactCode).toContain(
compact('<TimeSlider.Thumb className={cn(slider.thumb.base, slider.thumb.interactive)} />')
);
expect(compactCode).toContain(compact('<TimeSlider.Preview className={slider.preview}>'));
expect(compactCode).toContain(compact('<TimeSlider.Value type="pointer" className={slider.value} />'));
expect(compactCode).not.toContain('SliderTrack');
expect(compactCode).not.toContain('className={[');
});
});
@@ -2,12 +2,14 @@
import {
airplayIcon,
bufferingIndicator,
button,
buttonGroupEnd,
buttonGroupStart,
castIcon,
container,
controls,
error,
fullscreenIcon,
icon,
iconContainer,
@@ -18,6 +20,7 @@ import {
slider,
time,
} from '@videojs/skins/default/tailwind/video.tailwind';
import { isString } from '@videojs/utils/predicate';
import { cn } from '@videojs/utils/style';
import type { ReactNode } from 'react';
import {
@@ -33,41 +36,65 @@ import {
PlayIcon,
RestartIcon,
SeekIcon,
SpinnerIcon,
} from '@/icons';
import { Container } from '@/player/context';
import { AirPlayButton } from '@/ui/airplay-button';
import { BufferingIndicator } from '@/ui/buffering-indicator';
import { CastButton } from '@/ui/cast-button';
import { Controls } from '@/ui/controls';
import { ErrorDialog } from '@/ui/error-dialog';
import { FullscreenButton } from '@/ui/fullscreen-button';
import { Gesture } from '@/ui/gesture';
import { Hotkey } from '@/ui/hotkey';
import { PiPButton } from '@/ui/pip-button';
import { PlayButton } from '@/ui/play-button';
import { Poster } from '@/ui/poster';
import { SeekButton } from '@/ui/seek-button';
import { StatusAnnouncer } from '@/ui/status-announcer';
import { Time } from '@/ui/time';
import { TimeSlider } from '@/ui/time-slider';
import { Tooltip } from '@/ui/tooltip';
import { isRenderProp } from '@/utils/use-render';
import type { BaseVideoSkinProps } from '../types';
const SEEK_TIME = 10;
export interface DefaultVideoSkinProps {
className?: string | undefined;
const iconButton = [button.base, button.subtle, button.icon];
export interface DefaultVideoSkinProps extends BaseVideoSkinProps {
className?: string;
children?: ReactNode | undefined;
}
export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps) {
export function DefaultVideoSkin({ className, children, poster, ...rest }: DefaultVideoSkinProps) {
return (
<Container className={cn(container, className)}>
<Container className={cn(container, className)} {...rest}>
{children}
{poster && (
<Poster src={isString(poster) ? poster : undefined} render={isRenderProp(poster) ? poster : undefined} />
)}
<BufferingIndicator className={bufferingIndicator.root}>
<SpinnerIcon className={icon} />
</BufferingIndicator>
<ErrorDialog.Root>
<ErrorDialog.Popup className={error.popup}>
<ErrorDialog.Title className={error.title}>Something went wrong.</ErrorDialog.Title>
<ErrorDialog.Description className={error.description} />
<ErrorDialog.Close className={cn(button.base, button.primary, error.close)}>OK</ErrorDialog.Close>
</ErrorDialog.Popup>
</ErrorDialog.Root>
<Controls.Root className={controls} data-controls="">
<Tooltip.Provider>
<Controls.Group className={buttonGroupStart}>
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PlayButton className={cn(button.base, button.subtle, button.icon, playIcon.button)}>
<PlayButton className={cn(iconButton, playIcon.button)} type="button">
<RestartIcon className={cn(icon, playIcon.restart)} />
<PlayIcon className={cn(icon, playIcon.play)} />
<PauseIcon className={cn(icon, playIcon.pause)} />
@@ -83,7 +110,7 @@ export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps)
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={-SEEK_TIME} className={cn(button.base, button.subtle, button.icon)}>
<SeekButton seconds={-SEEK_TIME} className={cn(iconButton)} type="button">
<SeekIcon className={cn(icon, iconContainer, iconFlipped)} />
</SeekButton>
}
@@ -97,7 +124,7 @@ export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps)
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<SeekButton seconds={SEEK_TIME} className={cn(button.base, button.subtle, button.icon)}>
<SeekButton seconds={SEEK_TIME} className={cn(iconButton)} type="button">
<SeekIcon className={cn(icon, iconContainer)} />
</SeekButton>
}
@@ -128,7 +155,7 @@ export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps)
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<CastButton className={cn(button.base, button.subtle, button.icon, castIcon.button)}>
<CastButton className={cn(iconButton, castIcon.button)} type="button">
<CastEnterIcon className={cn(icon, castIcon.enter)} />
<CastExitIcon className={cn(icon, castIcon.exit)} />
</CastButton>
@@ -143,7 +170,7 @@ export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps)
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<AirPlayButton className={cn(button.base, button.subtle, button.icon, airplayIcon.button)}>
<AirPlayButton className={cn(iconButton, airplayIcon.button)} type="button">
<AirPlayEnterIcon className={cn(icon, airplayIcon.enter)} />
<AirPlayExitIcon className={cn(icon, airplayIcon.exit)} />
</AirPlayButton>
@@ -158,7 +185,7 @@ export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps)
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<PiPButton className={cn(button.base, button.subtle, button.icon, pipIcon.button)}>
<PiPButton className={cn(iconButton, pipIcon.button)} type="button">
<PipEnterIcon className={cn(icon, pipIcon.off)} />
<PipExitIcon className={cn(icon, pipIcon.on)} />
</PiPButton>
@@ -173,7 +200,7 @@ export function DefaultVideoSkin({ className, children }: DefaultVideoSkinProps)
<Tooltip.Root side="top">
<Tooltip.Trigger
render={
<FullscreenButton className={cn(button.base, button.subtle, button.icon, fullscreenIcon.button)}>
<FullscreenButton className={cn(iconButton, fullscreenIcon.button)} type="button">
<FullscreenEnterIcon className={cn(icon, fullscreenIcon.enter)} />
<FullscreenExitIcon className={cn(icon, fullscreenIcon.exit)} />
</FullscreenButton>