mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
docs(site): add "No Skin" option to installation skin picker (#1525)
This commit is contained in:
@@ -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');
|
||||
|
||||
Vendored
+1
-1
@@ -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[]>;
|
||||
|
||||
@@ -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'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import '../define/audio/player';
|
||||
@@ -0,0 +1 @@
|
||||
import '../define/video/player';
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user