docs(site): add "No Skin" option to installation skin picker (#1525)

This commit is contained in:
Ronald Urbina
2026-06-18 12:07:23 -03:00
committed by GitHub
parent 07428519a5
commit 0b1491c30e
15 changed files with 146 additions and 44 deletions
+1 -1
View File
@@ -106,7 +106,7 @@ const DOCS_HELP = `Usage: @videojs/cli docs <slug> [--framework <html|react>]
Installation flags (for docs how-to/installation):
--preset <video|audio|background-video>
--skin <default|minimal>
--skin <default|minimal|none>
--source-url <url>
--media <html5-video|html5-audio|hls|background-video>
--install-method <cdn|npm|pnpm|yarn|bun>`;
@@ -241,6 +241,14 @@ describe('handleDocs', () => {
expect(output()).toContain('minimal');
});
it('generates headless (no skin) variant with skin none', async () => {
await handleDocs(htmlFlags({ skin: 'none' }), ['how-to/installation']);
const out = output();
expect(out).toContain('<video-player>');
expect(out).not.toContain('<video-skin>');
expect(out).not.toContain("'@videojs/html/video/skin'");
});
it('includes custom source URL in generated code', async () => {
await handleDocs(htmlFlags({ 'source-url': 'https://example.com/my-video.mp4' }), ['how-to/installation']);
expect(output()).toContain('https://example.com/my-video.mp4');
+1 -1
View File
@@ -8,7 +8,7 @@
declare module '@/utils/installation/types' {
export type Renderer = 'background-video' | 'hls' | 'html5-audio' | 'html5-video';
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio';
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio' | 'none';
export type UseCase = 'default-video' | 'default-audio' | 'background-video';
export type InstallMethod = 'cdn' | 'npm' | 'pnpm' | 'yarn' | 'bun';
export const VALID_RENDERERS: Record<UseCase, Renderer[]>;
+3 -1
View File
@@ -46,6 +46,7 @@ function skinOptionsForUseCase(useCase: UseCase): Array<{ value: Skin; label: st
return [
{ value: isAudio ? 'audio' : 'video', label: 'Default' },
{ value: isAudio ? 'minimal-audio' : 'minimal-video', label: 'Minimal' },
{ value: 'none', label: 'None (headless)' },
];
}
@@ -76,10 +77,11 @@ export function mapRawSkin(skinFlag: string, useCase: UseCase): Skin {
const map: Record<string, Skin> = {
default: isAudio ? 'audio' : 'video',
minimal: isAudio ? 'minimal-audio' : 'minimal-video',
none: 'none',
};
const result = map[skinFlag];
if (!result) {
console.error(`Invalid skin: "${skinFlag}". Must be "default" or "minimal".`);
console.error(`Invalid skin: "${skinFlag}". Must be "default", "minimal", or "none".`);
process.exit(1);
}
return result;
@@ -51,4 +51,11 @@ describe('formatInstallationCode', () => {
const result = formatInstallationCode({ ...baseReact, installMethod: 'pnpm' });
expect(result).toContain('pnpm add @videojs/react');
});
it('formats HTML with skin none — omits skin tag and skin import', () => {
const result = formatInstallationCode({ ...baseHTML, skin: 'none' });
expect(result).toContain('<video-player>');
expect(result).not.toContain('<video-skin>');
expect(result).not.toContain("'@videojs/html/video/skin'");
});
});
+1
View File
@@ -0,0 +1 @@
import '../define/audio/player';
+1
View File
@@ -0,0 +1 @@
import '../define/video/player';
+2
View File
@@ -15,12 +15,14 @@ const buildModes: BuildMode[] = ['dev', 'prod'];
const presets = [
'video',
'video-headless',
'video-minimal',
'video-ui',
'video-minimal-ui',
'live-video',
'live-video-minimal',
'audio',
'audio-headless',
'audio-minimal',
'audio-ui',
'audio-minimal-ui',
@@ -1,5 +1,5 @@
import { useStore } from '@nanostores/react';
import { Minus, Sparkles } from 'lucide-react';
import { Code2, Minus, Sparkles } from 'lucide-react';
import { useEffect } from 'react';
import type { ImageRadioOption } from '@/components/ImageRadioGroup';
import ImageRadioGroup from '@/components/ImageRadioGroup';
@@ -9,11 +9,13 @@ import type { Skin } from '@/utils/installation/types';
const VIDEO_SKINS: ImageRadioOption<Skin>[] = [
{ value: 'video', label: 'Default', image: <Sparkles size={32} /> },
{ value: 'minimal-video', label: 'Minimal', image: <Minus size={32} /> },
{ value: 'none', label: 'No Skin', image: <Code2 size={32} /> },
];
const AUDIO_SKINS: ImageRadioOption<Skin>[] = [
{ value: 'audio', label: 'Default', image: <Sparkles size={32} /> },
{ value: 'minimal-audio', label: 'Minimal', image: <Minus size={32} /> },
{ value: 'none', label: 'No Skin', image: <Code2 size={32} /> },
];
export default function SkinPicker() {
@@ -42,7 +42,7 @@ If you run it without flags, it will allow you to interactively configure your p
npx @videojs/cli docs how-to/installation \
--framework <html|react> \
--preset <video|audio|background-video> \
--skin <default|minimal> \
--skin <default|minimal|none> \
--media <html5-video|html5-audio|hls|background-video> \
--source-url <url> \
--install-method <cdn|npm|pnpm|yarn|bun>
@@ -20,4 +20,16 @@ describe('generateCdnCode', () => {
`<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/background.js"></script>`
);
});
it('generates headless video CDN tag when skin is none', () => {
expect(generateCdnCode('default-video', 'none', 'html5-video')).toEqual(
`<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/video-headless.js"></script>`
);
});
it('generates headless audio CDN tag when skin is none', () => {
expect(generateCdnCode('default-audio', 'none', 'html5-audio')).toEqual(
`<script type="module" src="https://cdn.jsdelivr.net/npm/@videojs/html/cdn/audio-headless.js"></script>`
);
});
});
@@ -139,6 +139,14 @@ describe('generateHTMLUsageCode', () => {
expect(result.js).toContain("import '@videojs/html/video/minimal-skin'");
});
it('omits skin tag and skin import when skin is none', () => {
const result = generateHTMLUsageCode({ ...baseHTML, skin: 'none' });
expect(result.html).toContain('<video-player>');
expect(result.html).not.toContain('<video-skin>');
expect(result.js).toContain("import '@videojs/html/video/player'");
expect(result.js).not.toContain("import '@videojs/html/video/skin'");
});
it('uses custom source URL when provided', () => {
const result = generateHTMLUsageCode({ ...baseHTML, sourceUrl: 'https://example.com/video.mp4' });
expect(result.html).toContain('https://example.com/video.mp4');
@@ -193,6 +201,15 @@ describe('generateReactCreateCode', () => {
expect(code).toContain("import '@videojs/react/video/minimal-skin.css'");
});
it('omits skin component and CSS import when skin is none', () => {
const result = generateReactCreateCode({ ...baseReact, skin: 'none' });
const code = result['MyPlayer.tsx'];
expect(code).not.toContain('VideoSkin');
expect(code).not.toContain('skin.css');
expect(code).toContain('<Video src={src} playsInline />');
expect(code).toContain("from '@videojs/react/video'");
});
it('uses background video components', () => {
const opts: InstallationOptions = {
...baseReact,
+1
View File
@@ -4,6 +4,7 @@ const CDN_BASE = 'https://cdn.jsdelivr.net/npm/@videojs/html/cdn';
function getCdnFileName(useCase: UseCase, skin: Skin): string {
if (useCase === 'background-video') return 'background';
if (skin === 'none') return useCase === 'default-audio' ? 'audio-headless' : 'video-headless';
if (skin === 'minimal-video') return 'video-minimal';
if (skin === 'minimal-audio') return 'audio-minimal';
return skin;
+87 -38
View File
@@ -37,7 +37,11 @@ function isVideoLikeRenderer(renderer: Renderer): boolean {
return renderer === 'html5-video' || renderer === 'hls' || renderer === 'background-video';
}
function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
function getGroupFromUseCase(useCase: UseCase): string {
return useCase === 'default-audio' ? 'audio' : 'video';
}
function getSkinImportParts(skin: Exclude<Skin, 'none'>): { group: string; skinFile: string } {
if (skin === 'minimal-video') return { group: 'video', skinFile: 'minimal-skin' };
if (skin === 'minimal-audio') return { group: 'audio', skinFile: 'minimal-skin' };
return { group: skin, skinFile: 'skin' };
@@ -102,11 +106,11 @@ function getProviderTag(useCase: UseCase): string {
return map[useCase];
}
function getSkinTag(useCase: UseCase, skin: Skin): string {
function getSkinTag(useCase: UseCase, skin: Exclude<Skin, 'none'>): string {
if (useCase === 'background-video') {
return 'background-video-skin';
}
const map: Record<Skin, string> = {
const map: Record<Exclude<Skin, 'none'>, string> = {
video: 'video-skin',
audio: 'audio-skin',
'minimal-video': 'video-minimal-skin',
@@ -117,16 +121,38 @@ function getSkinTag(useCase: UseCase, skin: Skin): string {
function generateHTMLMarkup(useCase: UseCase, skin: Skin, renderer: Renderer, url: string): string {
const providerTag = getProviderTag(useCase);
const skinTag = getSkinTag(useCase, skin);
const tag = getRendererTag(renderer);
const src = resolveSourceUrl(url, renderer);
const playsInline = isVideoLikeRenderer(renderer) ? ' playsinline' : '';
return `<!--
const mediaComment = ` <!--
Media are players without UIs, handling networking
and display of the media. They are easily swappable
to handle different sources.
-->`;
const skinMediaComment = ` <!--
Media are players without UIs, handling networking
and display of the media. They are easily swappable
to handle different sources.
-->`;
const providerComment = `<!--
The PlayerProvider passes state between the UI components
and Media, and makes fully custom UIs possible.
It does not have layout by default (display:contents)
-->
-->`;
if (skin === 'none' && useCase !== 'background-video') {
return `${providerComment}
<${providerTag}>
${mediaComment}
<${tag} src="${src}"${playsInline}></${tag}>
</${providerTag}>`;
}
const skinTag = getSkinTag(useCase, skin as Exclude<Skin, 'none'>);
return `${providerComment}
<${providerTag}>
<!--
Skins contain the entire player UI and are easily swappable.
@@ -134,11 +160,7 @@ function generateHTMLMarkup(useCase: UseCase, skin: Skin, renderer: Renderer, ur
of UI components.
-->
<${skinTag}>
<!--
Media are players without UIs, handling networking
and display of the media. They are easily swappable
to handle different sources.
-->
${skinMediaComment}
<${tag} src="${src}"${playsInline}></${tag}>
</${skinTag}>
</${providerTag}>`;
@@ -152,9 +174,13 @@ function generateHTMLJSImports(useCase: UseCase, skin: Skin, renderer: Renderer)
import '@videojs/html/background/skin';
import '@videojs/html/background/video';${mediaImport}`;
}
const { group, skinFile } = getSkinImportParts(skin);
const group = skin === 'none' ? getGroupFromUseCase(useCase) : getSkinImportParts(skin).group;
const mediaSubpath = getMediaImportSubpath(renderer);
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
if (skin === 'none') {
return `import '@videojs/html/${group}/player';${mediaImport}`;
}
const { skinFile } = getSkinImportParts(skin);
return `import '@videojs/html/${group}/player';
import '@videojs/html/${group}/${skinFile}';${mediaImport}`;
}
@@ -181,8 +207,8 @@ function getRendererComponent(renderer: Renderer): string {
return map[renderer];
}
function getSkinComponent(skin: Skin): string {
const map: Record<Skin, string> = {
function getSkinComponent(skin: Exclude<Skin, 'none'>): string {
const map: Record<Exclude<Skin, 'none'>, string> = {
video: 'VideoSkin',
audio: 'AudioSkin',
'minimal-video': 'MinimalVideoSkin',
@@ -219,31 +245,58 @@ export function generateReactCreateCode(
const featureType = getUseCaseFeatures(useCase);
const isBackgroundVideo = useCase === 'background-video';
const skinComponent = isBackgroundVideo ? 'BackgroundVideoSkin' : getSkinComponent(skin);
const { group, skinFile } = getSkinImportParts(skin);
const skinCssImport = isBackgroundVideo
? '@videojs/react/background/skin.css'
: `@videojs/react/${group}/${skinFile}.css`;
const presetSubpath = isBackgroundVideo ? 'background' : group;
let presetImport: string;
let mediaImport: string | null = null;
if (isPresetRenderer(renderer)) {
presetImport = `import { ${skinComponent}, ${rendererComponent} } from '@videojs/react/${presetSubpath}';`;
} else {
presetImport = `import { ${skinComponent} } from '@videojs/react/${presetSubpath}';`;
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
}
const isNoSkin = skin === 'none';
const group = isBackgroundVideo
? 'background'
: isNoSkin
? getGroupFromUseCase(useCase)
: getSkinImportParts(skin).group;
const rendererProps = isVideoLikeRenderer(renderer) ? 'src={src} playsInline' : 'src={src}';
const rendererJsx = `<${rendererComponent} ${rendererProps} />`;
let presetImport: string;
let mediaImport: string | null = null;
let skinCssImport: string | null = null;
let skinComponent: string | null = null;
if (isBackgroundVideo) {
skinComponent = 'BackgroundVideoSkin';
skinCssImport = '@videojs/react/background/skin.css';
presetImport = `import { ${skinComponent}, ${rendererComponent} } from '@videojs/react/background';`;
} else if (isNoSkin) {
if (isPresetRenderer(renderer)) {
presetImport = `import { ${rendererComponent} } from '@videojs/react/${group}';`;
} else {
presetImport = '';
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
}
} else {
const { skinFile } = getSkinImportParts(skin);
skinComponent = getSkinComponent(skin);
skinCssImport = `@videojs/react/${group}/${skinFile}.css`;
if (isPresetRenderer(renderer)) {
presetImport = `import { ${skinComponent}, ${rendererComponent} } from '@videojs/react/${group}';`;
} else {
presetImport = `import { ${skinComponent} } from '@videojs/react/${group}';`;
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
}
}
const playerJsx = skinComponent
? ` <Player.Provider>
<${skinComponent}>
${rendererJsx}
</${skinComponent}>
</Player.Provider>`
: ` <Player.Provider>
${rendererJsx}
</Player.Provider>`;
const imports = [
`import '${skinCssImport}';`,
...(skinCssImport ? [`import '${skinCssImport}';`] : []),
`import { createPlayer, ${featureType} } from '@videojs/react';`,
presetImport,
...(presetImport ? [presetImport] : []),
...(mediaImport ? [mediaImport] : []),
].join('\n');
@@ -260,11 +313,7 @@ interface MyPlayerProps {
export const MyPlayer = ({ src }: MyPlayerProps) => {
return (
<Player.Provider>
<${skinComponent}>
${rendererJsx}
</${skinComponent}>
</Player.Provider>
${playerJsx}
);
};`,
};
+1 -1
View File
@@ -1,6 +1,6 @@
export type Renderer = 'background-video' | 'hls' | 'html5-audio' | 'html5-video';
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio';
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio' | 'none';
export type UseCase = 'default-video' | 'default-audio' | 'background-video';