mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
fix(icons): use icon exports in ejected skins (#1489)
This commit is contained in:
@@ -296,7 +296,8 @@ function ejectedHtmlPage(): string {
|
||||
// Path from pages/ to the site content
|
||||
const jsonPath = '../../../../../../site/src/content/ejected-skins.json';
|
||||
|
||||
return `import '@videojs/html/video/ui';
|
||||
return `import '@videojs/html/icons/element';
|
||||
import '@videojs/html/video/ui';
|
||||
import ejectedSkins from '${jsonPath}';
|
||||
|
||||
interface EjectedSkinEntry {
|
||||
|
||||
@@ -14,7 +14,7 @@ import '@videojs/html/media/container';
|
||||
import '@videojs/html/media/simple-hls-video';
|
||||
import '@videojs/html/ui/play-button';
|
||||
import '@videojs/html/ui/mute-button';
|
||||
import { pauseIcon, playIcon, restartIcon, volumeHighIcon, volumeOffIcon } from '@videojs/icons/html';
|
||||
import { pauseIcon, playIcon, restartIcon, volumeHighIcon, volumeOffIcon } from '@videojs/html/icons';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '@app/styles.css';
|
||||
|
||||
// SimpleHlsVideo + Video.js integration sandbox — React
|
||||
// http://localhost:5173/simple-hls-react/
|
||||
//
|
||||
@@ -6,8 +7,8 @@ import '@app/styles.css';
|
||||
// player with play/mute controls. SimpleHlsVideo registers itself via
|
||||
// useMediaAttach so the store discovers it without any querySelector.
|
||||
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeOffIcon } from '@videojs/icons/react';
|
||||
import { Container, createPlayer, MuteButton, PlayButton } from '@videojs/react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeOffIcon } from '@videojs/react/icons';
|
||||
import { SimpleHlsVideo } from '@videojs/react/media/simple-hls-video';
|
||||
import { videoFeatures } from '@videojs/react/video';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"types": "dist/dev/index.d.ts",
|
||||
"sideEffects": [
|
||||
"./dist/*/define/**/*.js",
|
||||
"./dist/*/icons/element/**/*.js",
|
||||
"./dist/*/icons/dist/element/**/*.js",
|
||||
"./cdn/**/*.js"
|
||||
],
|
||||
"files": [
|
||||
@@ -26,6 +28,16 @@
|
||||
"development": "./dist/dev/index.js",
|
||||
"default": "./dist/default/index.js"
|
||||
},
|
||||
"./icons": {
|
||||
"types": "./dist/dev/icons/index.d.ts",
|
||||
"development": "./dist/dev/icons/index.js",
|
||||
"default": "./dist/default/icons/index.js"
|
||||
},
|
||||
"./icons/*": {
|
||||
"types": "./dist/dev/icons/*/index.d.ts",
|
||||
"development": "./dist/dev/icons/*/index.js",
|
||||
"default": "./dist/default/icons/*/index.js"
|
||||
},
|
||||
"./video": {
|
||||
"types": "./dist/dev/presets/video.d.ts",
|
||||
"development": "./dist/dev/presets/video.js",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
import '../icons/element/minimal';
|
||||
import '../define/audio/minimal-ui';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
import '../icons/element/default';
|
||||
import '../define/audio/ui';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
import '../icons/element/minimal';
|
||||
import '../define/video/minimal-ui';
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
import '../icons/element/default';
|
||||
import '../define/video/ui';
|
||||
|
||||
@@ -60,9 +60,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="${cn(button.base, button.subtle, button.icon)}">
|
||||
<span class="${iconContainer}">
|
||||
@@ -70,9 +68,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="${time.controls}">
|
||||
@@ -94,9 +90,7 @@ function getTemplateHTML() {
|
||||
<div class="${buttonGroup}">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="${cn(button.base, button.subtle, button.icon, playbackRate.button)}">
|
||||
</media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="${cn(button.base, button.subtle, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
|
||||
@@ -44,9 +44,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-tooltip">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="media-button media-button--subtle media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
@@ -54,9 +52,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-tooltip">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="media-time-controls">
|
||||
@@ -78,9 +74,7 @@ function getTemplateHTML() {
|
||||
<div class="media-button-group">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--subtle media-button--icon media-button--playback-rate">
|
||||
</media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-tooltip">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="media-button media-button--subtle media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
|
||||
@@ -60,9 +60,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="${cn(button.base, button.subtle, button.icon)}">
|
||||
<span class="${iconContainer}">
|
||||
@@ -70,9 +68,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="${time.group}">
|
||||
@@ -89,9 +85,7 @@ function getTemplateHTML() {
|
||||
|
||||
<div class="${buttonGroup}">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="${cn(button.base, button.subtle, button.icon, playbackRate.button)}"></media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="${cn(button.base, button.subtle, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
|
||||
@@ -44,9 +44,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-surface media-tooltip">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="media-button media-button--subtle media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
@@ -54,9 +52,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-surface media-tooltip">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="media-time-controls">
|
||||
@@ -73,9 +69,7 @@ function getTemplateHTML() {
|
||||
|
||||
<div class="media-button-group">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--subtle media-button--icon media-button--playback-rate"></media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-surface media-tooltip">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="media-button media-button--subtle media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
|
||||
@@ -67,9 +67,7 @@ function getTemplateHTML() {
|
||||
${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })}
|
||||
${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })}
|
||||
</media-captions-button>
|
||||
<media-tooltip id="captions-tooltip" side="top" class="media-tooltip">
|
||||
Toggle captions
|
||||
</media-tooltip>
|
||||
<media-tooltip id="captions-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
|
||||
<media-cast-button commandfor="cast-tooltip" class="media-button media-button--subtle media-button--icon media-button--cast">
|
||||
${renderIcon('cast-enter', { class: 'media-icon media-icon--cast-enter' })}
|
||||
|
||||
@@ -73,9 +73,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="${cn(button.base, button.subtle, button.icon)}">
|
||||
<span class="${iconContainer}">
|
||||
@@ -83,9 +81,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="${time.controls}">
|
||||
@@ -115,9 +111,7 @@ function getTemplateHTML() {
|
||||
<div class="${buttonGroupEnd}">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="${cn(button.base, button.subtle, button.icon, playbackRate.button)}">
|
||||
</media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="video-volume-popover" class="${cn(button.base, button.subtle, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
|
||||
@@ -52,9 +52,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-tooltip">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="media-button media-button--subtle media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
@@ -62,9 +60,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-tooltip">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="media-time-controls">
|
||||
@@ -93,9 +89,7 @@ function getTemplateHTML() {
|
||||
|
||||
<div class="media-button-group">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--subtle media-button--icon media-button--playback-rate"></media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-tooltip">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--subtle media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
@@ -116,9 +110,7 @@ function getTemplateHTML() {
|
||||
${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })}
|
||||
${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })}
|
||||
</media-captions-button>
|
||||
<media-tooltip id="captions-tooltip" side="top" class="media-tooltip">
|
||||
Toggle captions
|
||||
</media-tooltip>
|
||||
<media-tooltip id="captions-tooltip" side="top" class="media-tooltip"></media-tooltip>
|
||||
|
||||
<media-cast-button commandfor="cast-tooltip" class="media-button media-button--subtle media-button--icon media-button--cast">
|
||||
${renderIcon('cast-enter', { class: 'media-icon media-icon--cast-enter' })}
|
||||
|
||||
@@ -75,9 +75,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="${cn(button.base, button.subtle, button.icon)}">
|
||||
<span class="${iconContainer}">
|
||||
@@ -85,9 +83,7 @@ function getTemplateHTML() {
|
||||
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="${time.group}">
|
||||
@@ -110,9 +106,7 @@ function getTemplateHTML() {
|
||||
|
||||
<div class="${buttonGroupEnd}">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="${cn(button.base, button.subtle, button.icon, playbackRate.button)}"></media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="${cn(popup.tooltip)}"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="video-volume-popover" class="${cn(button.base, button.subtle, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
|
||||
@@ -54,9 +54,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-surface media-tooltip">
|
||||
Seek backward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-backward-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
|
||||
|
||||
<media-seek-button commandfor="seek-forward-tooltip" seconds="${SEEK_TIME}" class="media-button media-button--subtle media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
@@ -64,9 +62,7 @@ function getTemplateHTML() {
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-surface media-tooltip">
|
||||
Seek forward ${SEEK_TIME} seconds
|
||||
</media-tooltip>
|
||||
<media-tooltip id="seek-forward-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="media-time-controls">
|
||||
@@ -89,9 +85,7 @@ function getTemplateHTML() {
|
||||
|
||||
<div class="media-button-group">
|
||||
<media-playback-rate-button commandfor="playback-rate-tooltip" class="media-button media-button--subtle media-button--icon media-button--playback-rate"></media-playback-rate-button>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-surface media-tooltip">
|
||||
Toggle playback rate
|
||||
</media-tooltip>
|
||||
<media-tooltip id="playback-rate-tooltip" side="top" class="media-surface media-tooltip"></media-tooltip>
|
||||
|
||||
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--subtle media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@videojs/icons/html/default';
|
||||
@@ -0,0 +1 @@
|
||||
import '@videojs/icons/element/default';
|
||||
@@ -0,0 +1 @@
|
||||
import '@videojs/icons/element';
|
||||
@@ -0,0 +1 @@
|
||||
import '@videojs/icons/element/minimal';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@videojs/icons/html';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@videojs/icons/html/minimal';
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Window } from 'happy-dom';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('@videojs/html/icons', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('exports SVG strings without registering media-icon', async () => {
|
||||
const testWindow = new Window();
|
||||
|
||||
vi.stubGlobal('window', testWindow);
|
||||
vi.stubGlobal('document', testWindow.document);
|
||||
vi.stubGlobal('customElements', testWindow.customElements);
|
||||
vi.stubGlobal('HTMLElement', testWindow.HTMLElement);
|
||||
|
||||
const { playIcon } = await import('../index');
|
||||
|
||||
expect(playIcon).toContain('<svg');
|
||||
expect(customElements.get('media-icon')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renders icons that exist before the registry module loads', async () => {
|
||||
const testWindow = new Window();
|
||||
|
||||
vi.stubGlobal('window', testWindow);
|
||||
vi.stubGlobal('document', testWindow.document);
|
||||
vi.stubGlobal('customElements', testWindow.customElements);
|
||||
vi.stubGlobal('HTMLElement', testWindow.HTMLElement);
|
||||
|
||||
document.body.innerHTML = '<media-icon name="play"></media-icon>';
|
||||
|
||||
await import('../element');
|
||||
await customElements.whenDefined('media-icon');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.querySelector('media-icon')?.innerHTML).toContain('<svg');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders minimal icons from the lazy family loader', async () => {
|
||||
const testWindow = new Window();
|
||||
|
||||
vi.stubGlobal('window', testWindow);
|
||||
vi.stubGlobal('document', testWindow.document);
|
||||
vi.stubGlobal('customElements', testWindow.customElements);
|
||||
vi.stubGlobal('HTMLElement', testWindow.HTMLElement);
|
||||
|
||||
document.body.innerHTML = '<media-icon family="minimal" name="play"></media-icon>';
|
||||
|
||||
await import('../element');
|
||||
await customElements.whenDefined('media-icon');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.querySelector('media-icon')?.innerHTML).toContain('<svg');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not microtask-loop when icon name is missing from a loaded family', async () => {
|
||||
const testWindow = new Window();
|
||||
|
||||
vi.stubGlobal('window', testWindow);
|
||||
vi.stubGlobal('document', testWindow.document);
|
||||
vi.stubGlobal('customElements', testWindow.customElements);
|
||||
vi.stubGlobal('HTMLElement', testWindow.HTMLElement);
|
||||
|
||||
document.body.innerHTML = `
|
||||
<media-icon id="good" name="play"></media-icon>
|
||||
<media-icon id="bad" name="__no_such_icon__"></media-icon>
|
||||
`;
|
||||
|
||||
await import('../element');
|
||||
await customElements.whenDefined('media-icon');
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.querySelector('#good')?.innerHTML).toContain('<svg');
|
||||
});
|
||||
|
||||
expect(document.querySelector('#bad')?.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('does not microtask-loop when family has no loader', async () => {
|
||||
const testWindow = new Window();
|
||||
|
||||
vi.stubGlobal('window', testWindow);
|
||||
vi.stubGlobal('document', testWindow.document);
|
||||
vi.stubGlobal('customElements', testWindow.customElements);
|
||||
vi.stubGlobal('HTMLElement', testWindow.HTMLElement);
|
||||
|
||||
document.body.innerHTML = '<media-icon family="__unknown_family__" name="play"></media-icon>';
|
||||
|
||||
await import('../element');
|
||||
await customElements.whenDefined('media-icon');
|
||||
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
expect(document.querySelector('media-icon')?.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('renders icons from a family-specific import', async () => {
|
||||
const testWindow = new Window();
|
||||
|
||||
vi.stubGlobal('window', testWindow);
|
||||
vi.stubGlobal('document', testWindow.document);
|
||||
vi.stubGlobal('customElements', testWindow.customElements);
|
||||
vi.stubGlobal('HTMLElement', testWindow.HTMLElement);
|
||||
|
||||
document.body.innerHTML = '<media-icon family="minimal" name="play"></media-icon>';
|
||||
|
||||
await import('../element/minimal');
|
||||
await customElements.whenDefined('media-icon');
|
||||
|
||||
expect(document.querySelector('media-icon')?.innerHTML).toContain('<svg');
|
||||
});
|
||||
});
|
||||
@@ -83,7 +83,10 @@ for (const mode of buildModes) {
|
||||
noExternal: [/.*/],
|
||||
inlineOnly: false,
|
||||
treeshake: {
|
||||
moduleSideEffects: [{ test: /\/define\//, sideEffects: true }],
|
||||
moduleSideEffects: [
|
||||
{ test: /\/define\//, sideEffects: true },
|
||||
{ test: /\/icons\/(?:dist\/)?element\//, sideEffects: true },
|
||||
],
|
||||
},
|
||||
outDir,
|
||||
alias: {
|
||||
|
||||
@@ -29,9 +29,17 @@ const presetEntries = Object.fromEntries(
|
||||
})
|
||||
);
|
||||
|
||||
const iconEntries = Object.fromEntries(
|
||||
globSync('src/icons/**/index.ts').map((file) => {
|
||||
const key = file.replace('src/', '').replace('.ts', '');
|
||||
return [key, file];
|
||||
})
|
||||
);
|
||||
|
||||
const createConfig = (mode: BuildMode): UserConfig => ({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
...iconEntries,
|
||||
...defineEntries,
|
||||
...presetEntries,
|
||||
},
|
||||
@@ -45,7 +53,10 @@ const createConfig = (mode: BuildMode): UserConfig => ({
|
||||
// The sideEffects field in package.json uses dist paths, but the build
|
||||
// runs against source. Ensure define/* modules (which register custom
|
||||
// elements as a side effect) are never tree-shaken from skin bundles.
|
||||
moduleSideEffects: [{ test: /\/define\//, sideEffects: true }],
|
||||
moduleSideEffects: [
|
||||
{ test: /\/define\//, sideEffects: true },
|
||||
{ test: /\/icons\/(?:dist\/)?element\//, sideEffects: true },
|
||||
],
|
||||
},
|
||||
noExternal: [/^@videojs\/icons/, /^@videojs\/skins/],
|
||||
alias: {
|
||||
|
||||
@@ -35,7 +35,7 @@ Icons are grouped into visual sets. Each set contains the same icon names with d
|
||||
Import icons as React components. They accept standard SVG props and support ref forwarding.
|
||||
|
||||
```tsx
|
||||
import { PlayIcon, PauseIcon } from '@videojs/icons/react';
|
||||
import { PlayIcon, PauseIcon } from '@videojs/react/icons';
|
||||
|
||||
function Controls() {
|
||||
return (
|
||||
@@ -49,7 +49,7 @@ function Controls() {
|
||||
Import from a specific icon set:
|
||||
|
||||
```tsx
|
||||
import { PlayIcon } from '@videojs/icons/react/minimal';
|
||||
import { PlayIcon } from '@videojs/react/icons/minimal';
|
||||
```
|
||||
|
||||
### HTML
|
||||
@@ -57,7 +57,7 @@ import { PlayIcon } from '@videojs/icons/react/minimal';
|
||||
Import icons as SVG strings for use in plain HTML or web components.
|
||||
|
||||
```ts
|
||||
import { playIcon, pauseIcon } from '@videojs/icons/html';
|
||||
import { playIcon, pauseIcon } from '@videojs/html/icons';
|
||||
|
||||
button.innerHTML = playIcon;
|
||||
```
|
||||
@@ -65,7 +65,19 @@ button.innerHTML = playIcon;
|
||||
Import from a specific icon set:
|
||||
|
||||
```ts
|
||||
import { playIcon } from '@videojs/icons/html/minimal';
|
||||
import { playIcon } from '@videojs/html/icons/minimal';
|
||||
```
|
||||
|
||||
Register the `<media-icon>` custom element separately when you want HTML skins to render icons lazily:
|
||||
|
||||
```ts
|
||||
import '@videojs/html/icons/element';
|
||||
```
|
||||
|
||||
Import a specific element icon set when eager family registration is preferred:
|
||||
|
||||
```ts
|
||||
import '@videojs/html/icons/element/minimal';
|
||||
```
|
||||
|
||||
## Styling
|
||||
@@ -100,8 +112,8 @@ The build automatically:
|
||||
Create a new directory under `src/assets/` (e.g., `src/assets/custom/`), add SVG files, and rebuild. The set is automatically available via:
|
||||
|
||||
```ts
|
||||
import { PlayIcon } from '@videojs/icons/react/custom';
|
||||
import { playIcon } from '@videojs/icons/html/custom';
|
||||
import { PlayIcon } from '@videojs/react/icons/custom';
|
||||
import { playIcon } from '@videojs/html/icons/custom';
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
"./element": {
|
||||
"types": "./dist/element/index.d.ts",
|
||||
"default": "./dist/element/index.js"
|
||||
},
|
||||
"./element/*": {
|
||||
"types": "./dist/element/*/index.d.ts",
|
||||
"default": "./dist/element/*/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -128,19 +128,43 @@ function buildIconMap(icons: { name: string; content: string }[]): string {
|
||||
}
|
||||
|
||||
function buildElementIndex(sets: string[]): string {
|
||||
const varName = (set: string) => `${camelCase(set)}Icons`;
|
||||
const imports = sets.map((set) => `import { icons as ${varName(set)} } from './${set}/icons.js';`).join('\n');
|
||||
const registers = sets.map((set) => `MediaIconElement.register('${set}', ${varName(set)});`).join('\n');
|
||||
const loaders = sets
|
||||
.map(
|
||||
(set) =>
|
||||
` mediaIconElement.registerLoader?.('${set}', () => import('./${set}/icons.js').then((module) => module.icons));`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return [
|
||||
`import { MediaIconElement } from './base.js';`,
|
||||
imports,
|
||||
``,
|
||||
`if (!customElements.get('media-icon')) {`,
|
||||
` customElements.define('media-icon', MediaIconElement);`,
|
||||
`if (typeof customElements !== 'undefined' && typeof HTMLElement !== 'undefined') {`,
|
||||
` const mediaIconElement = customElements.get('media-icon') || MediaIconElement;`,
|
||||
``,
|
||||
loaders,
|
||||
``,
|
||||
` if (!customElements.get('media-icon')) {`,
|
||||
` customElements.define('media-icon', MediaIconElement);`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
registers,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildElementFamilyIndex(set: string): string {
|
||||
return [
|
||||
`import { MediaIconElement } from '../base.js';`,
|
||||
`import { icons } from './icons.js';`,
|
||||
``,
|
||||
`if (typeof customElements !== 'undefined' && typeof HTMLElement !== 'undefined') {`,
|
||||
` const mediaIconElement = customElements.get('media-icon') || MediaIconElement;`,
|
||||
``,
|
||||
` mediaIconElement.register?.('${set}', icons);`,
|
||||
``,
|
||||
` if (!customElements.get('media-icon')) {`,
|
||||
` customElements.define('media-icon', MediaIconElement);`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -149,6 +173,9 @@ function buildElementBase(): string {
|
||||
return [
|
||||
`export class MediaIconElement extends HTMLElement {`,
|
||||
` static #families = new Map();`,
|
||||
` static #loaders = new Map();`,
|
||||
` static #loading = new Map();`,
|
||||
` static #instances = new Set();`,
|
||||
``,
|
||||
` static register(family, icons) {`,
|
||||
` const map = MediaIconElement.#families.get(family) ?? new Map();`,
|
||||
@@ -156,6 +183,37 @@ function buildElementBase(): string {
|
||||
` map.set(name, svg);`,
|
||||
` }`,
|
||||
` MediaIconElement.#families.set(family, map);`,
|
||||
` MediaIconElement.#renderFamily(family);`,
|
||||
` }`,
|
||||
``,
|
||||
` static registerLoader(family, load) {`,
|
||||
` MediaIconElement.#loaders.set(family, load);`,
|
||||
` }`,
|
||||
``,
|
||||
` static load(family) {`,
|
||||
` if (MediaIconElement.#families.has(family)) return Promise.resolve();`,
|
||||
``,
|
||||
` const pending = MediaIconElement.#loading.get(family);`,
|
||||
` if (pending) return pending;`,
|
||||
``,
|
||||
` const loader = MediaIconElement.#loaders.get(family);`,
|
||||
` if (!loader) return Promise.resolve();`,
|
||||
``,
|
||||
` const loading = Promise.resolve()`,
|
||||
` .then(() => loader())`,
|
||||
` .then((icons) => {`,
|
||||
` if (icons) MediaIconElement.register(family, icons);`,
|
||||
` })`,
|
||||
` .finally(() => MediaIconElement.#loading.delete(family));`,
|
||||
``,
|
||||
` MediaIconElement.#loading.set(family, loading);`,
|
||||
` return loading;`,
|
||||
` }`,
|
||||
``,
|
||||
` static #renderFamily(family) {`,
|
||||
` for (const icon of MediaIconElement.#instances) {`,
|
||||
` if (icon.#family === family) icon.#render();`,
|
||||
` }`,
|
||||
` }`,
|
||||
``,
|
||||
` static get observedAttributes() {`,
|
||||
@@ -167,17 +225,41 @@ function buildElementBase(): string {
|
||||
` }`,
|
||||
``,
|
||||
` connectedCallback() {`,
|
||||
` MediaIconElement.#instances.add(this);`,
|
||||
` this.#render();`,
|
||||
` }`,
|
||||
``,
|
||||
` disconnectedCallback() {`,
|
||||
` MediaIconElement.#instances.delete(this);`,
|
||||
` }`,
|
||||
``,
|
||||
` get #family() {`,
|
||||
` return this.getAttribute('family') || 'default';`,
|
||||
` }`,
|
||||
``,
|
||||
` #render() {`,
|
||||
` const name = this.getAttribute('name');`,
|
||||
` if (!name) return;`,
|
||||
``,
|
||||
` const family = this.getAttribute('family') || 'default';`,
|
||||
` const family = this.#family;`,
|
||||
` const icons = MediaIconElement.#families.get(family);`,
|
||||
` const svg = icons?.get(name);`,
|
||||
` if (!svg) return;`,
|
||||
` if (!svg) {`,
|
||||
` if (MediaIconElement.#families.has(family)) return;`,
|
||||
``,
|
||||
` MediaIconElement.load(family).then(() => {`,
|
||||
` if (`,
|
||||
` !this.isConnected ||`,
|
||||
` this.getAttribute('name') !== name ||`,
|
||||
` this.#family !== family ||`,
|
||||
` !MediaIconElement.#families.has(family)`,
|
||||
` ) {`,
|
||||
` return;`,
|
||||
` }`,
|
||||
` this.#render();`,
|
||||
` }, () => {});`,
|
||||
` return;`,
|
||||
` }`,
|
||||
``,
|
||||
` this.innerHTML = svg;`,
|
||||
` }`,
|
||||
@@ -189,10 +271,14 @@ function buildElementBase(): string {
|
||||
function buildElementBaseTypes(): string {
|
||||
return [
|
||||
`export type IconMap = Record<string, string>;`,
|
||||
`export type IconLoader = () => IconMap | Promise<IconMap>;`,
|
||||
``,
|
||||
`export declare class MediaIconElement extends HTMLElement {`,
|
||||
` static register(family: string, icons: IconMap): void;`,
|
||||
` static registerLoader(family: string, load: IconLoader): void;`,
|
||||
` static load(family: string): Promise<void>;`,
|
||||
` connectedCallback(): void;`,
|
||||
` disconnectedCallback(): void;`,
|
||||
` attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;`,
|
||||
`}`,
|
||||
``,
|
||||
@@ -288,6 +374,8 @@ async function buildIconSet(setName: string): Promise<void> {
|
||||
|
||||
writeFileSync(join(elementDir, 'icons.js'), buildIconMap(icons));
|
||||
writeFileSync(join(elementDir, 'icons.d.ts'), `export declare const icons: Record<string, string>;\n`);
|
||||
writeFileSync(join(elementDir, 'index.js'), buildElementFamilyIndex(setName));
|
||||
writeFileSync(join(elementDir, 'index.d.ts'), `export {};\n`);
|
||||
}
|
||||
|
||||
async function build(): Promise<void> {
|
||||
|
||||
@@ -22,6 +22,16 @@
|
||||
"development": "./dist/dev/index.js",
|
||||
"default": "./dist/default/index.js"
|
||||
},
|
||||
"./icons": {
|
||||
"types": "./dist/dev/icons/index.d.ts",
|
||||
"development": "./dist/dev/icons/index.js",
|
||||
"default": "./dist/default/icons/index.js"
|
||||
},
|
||||
"./icons/*": {
|
||||
"types": "./dist/dev/icons/*/index.d.ts",
|
||||
"development": "./dist/dev/icons/*/index.js",
|
||||
"default": "./dist/default/icons/*/index.js"
|
||||
},
|
||||
"./media/*": {
|
||||
"types": "./dist/dev/media/*/index.d.ts",
|
||||
"development": "./dist/dev/media/*/index.js",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@videojs/icons/react';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@videojs/icons/react/minimal';
|
||||
@@ -1,12 +1,3 @@
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import { playbackRate } from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import {
|
||||
button,
|
||||
@@ -25,6 +16,15 @@ import {
|
||||
} from '@videojs/skins/minimal/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
@@ -6,9 +8,7 @@ import {
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react';
|
||||
import {
|
||||
button,
|
||||
buttonGroup,
|
||||
@@ -25,6 +16,7 @@ import {
|
||||
} from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, SeekIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, SeekIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey/hotkey';
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import {
|
||||
button,
|
||||
buttonGroup,
|
||||
@@ -19,6 +11,7 @@ import {
|
||||
} from '@videojs/skins/minimal/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/icons/react';
|
||||
import {
|
||||
button,
|
||||
buttonGroup,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
} from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { MuteButton } from '@/ui/mute-button';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@videojs/icons/react';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import { PauseIcon, PlayIcon, RestartIcon, VolumeHighIcon, VolumeLowIcon, VolumeOffIcon } from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { ErrorDialog } from '@/ui/error-dialog';
|
||||
import { Hotkey } from '@/ui/hotkey/hotkey';
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import {
|
||||
bufferingIndicator,
|
||||
button,
|
||||
@@ -33,6 +16,23 @@ import {
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
@@ -14,10 +17,7 @@ import {
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react';
|
||||
import {
|
||||
bufferingIndicator,
|
||||
button,
|
||||
@@ -33,6 +16,23 @@ import {
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
@@ -14,10 +17,7 @@ import {
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import { playbackRate } from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import {
|
||||
bufferingIndicator,
|
||||
@@ -40,6 +22,24 @@ import {
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
@@ -15,10 +18,7 @@ import {
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react/minimal';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
} from '@/icons/minimal';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react';
|
||||
import {
|
||||
bufferingIndicator,
|
||||
button,
|
||||
@@ -40,6 +22,24 @@ import {
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
@@ -15,10 +18,7 @@ import {
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/react';
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { type ComponentProps, forwardRef, type ReactNode } from 'react';
|
||||
} from '@/icons';
|
||||
import { Container, usePlayer } from '@/player/context';
|
||||
import { BufferingIndicator } from '@/ui/buffering-indicator';
|
||||
import { CaptionsButton } from '@/ui/captions-button';
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
* Build ejected skin snippets for copy-paste usage.
|
||||
*
|
||||
* Produces `site/src/content/ejected-skins.json` with:
|
||||
* - HTML skins: rendered HTML templates with inline SVGs and resolved classes
|
||||
* - React skins: TSX (with types) and JSX (types stripped) with inline SVGs
|
||||
* - HTML skins: rendered HTML templates with <media-icon> elements and resolved classes
|
||||
* - React skins: TSX (with types) and JSX (types stripped) with public icon imports
|
||||
* - CSS variants include a `css` field with all @imports resolved
|
||||
* - Tailwind variants omit the `css` field (users bring their own Tailwind)
|
||||
*
|
||||
* Prerequisites: `pnpm build:packages` (at minimum icons, skins, utils).
|
||||
* Prerequisites: `pnpm build:packages` (at minimum html, react, icons, skins, utils).
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, relative as relativePath, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import ts from 'typescript';
|
||||
@@ -284,7 +284,7 @@ function resolveRelativeModulePath(importerPath: string, specifier: string): str
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -410,7 +410,8 @@ function inlineModuleExport(
|
||||
return `${exportText}\n\n${aliasKeyword} ${localName} = ${importName};`;
|
||||
}
|
||||
|
||||
function inlineRelativeImports(source: string, sourcePath: string): string {
|
||||
function inlineRelativeImports(source: string, sourcePath: string, rewriteSource = (value: string) => value): string {
|
||||
source = rewriteSource(source);
|
||||
const sourceFile = createSourceFile(sourcePath, source);
|
||||
const declarationsToInline: string[] = [];
|
||||
const extraImports = new Set<string>();
|
||||
@@ -433,9 +434,9 @@ function inlineRelativeImports(source: string, sourcePath: string): string {
|
||||
}
|
||||
|
||||
const targetPath = resolveRelativeModulePath(sourcePath, specifier);
|
||||
const targetSource = readFileSync(targetPath, 'utf-8');
|
||||
const targetSource = rewriteSource(readFileSync(targetPath, 'utf-8'));
|
||||
validatePackageImports(targetSource, toRepoPath(targetPath));
|
||||
const transformedTargetSource = inlineRelativeImports(targetSource, targetPath);
|
||||
const transformedTargetSource = inlineRelativeImports(targetSource, targetPath, rewriteSource);
|
||||
const transformedTargetFile = createSourceFile(targetPath, transformedTargetSource);
|
||||
|
||||
for (const targetStatement of transformedTargetFile.statements) {
|
||||
@@ -736,13 +737,6 @@ function parseImportedNames(source: string): Map<string, string> {
|
||||
return imports;
|
||||
}
|
||||
|
||||
async function loadRenderIcon(
|
||||
iconSet: 'default' | 'minimal'
|
||||
): Promise<(name: string, attrs?: Record<string, string>) => string> {
|
||||
const mod = await import(pkgDistUrl(`@videojs/icons/render/${iconSet}`));
|
||||
return mod.renderIcon;
|
||||
}
|
||||
|
||||
async function loadCn(): Promise<(...args: unknown[]) => string> {
|
||||
const mod = await import(pkgDistUrl('@videojs/utils/style'));
|
||||
return mod.cn;
|
||||
@@ -779,6 +773,21 @@ function evaluateTemplate(templateBody: string, context: Record<string, unknown>
|
||||
.trim();
|
||||
}
|
||||
|
||||
function escapeAttributeValue(value: string): string {
|
||||
return value.replaceAll('&', '&').replaceAll('"', '"');
|
||||
}
|
||||
|
||||
function createRenderMediaIcon(iconSet: 'default' | 'minimal') {
|
||||
return (name: string, attrs?: Record<string, string>): string => {
|
||||
const family = iconSet === 'minimal' ? ' family="minimal"' : '';
|
||||
const attrText = Object.entries(attrs ?? {})
|
||||
.map(([key, value]) => ` ${key}="${escapeAttributeValue(value)}"`)
|
||||
.join('');
|
||||
|
||||
return `<media-icon name="${escapeAttributeValue(name)}"${family}${attrText}></media-icon>`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace `<slot name="media">`, `<slot>` (default slot), and
|
||||
* `<slot name="poster">` with concrete elements so the ejected HTML is
|
||||
@@ -812,12 +821,11 @@ async function processHtmlSkin(skin: HtmlSkinDef): Promise<string> {
|
||||
validatePackageImports(source, skin.template);
|
||||
const templateBody = extractTemplateLiteral(source);
|
||||
|
||||
const renderIcon = await loadRenderIcon(skin.iconSet);
|
||||
const cn = await loadCn();
|
||||
|
||||
// Build context object with all the variables the template needs
|
||||
const context: Record<string, unknown> = {
|
||||
renderIcon,
|
||||
renderIcon: createRenderMediaIcon(skin.iconSet),
|
||||
cn,
|
||||
SEEK_TIME: 10,
|
||||
};
|
||||
@@ -853,48 +861,9 @@ async function processHtmlSkin(skin: HtmlSkinDef): Promise<string> {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// React skin processing — inline SVGs, resolve imports, produce TSX + JSX
|
||||
// React skin processing - resolve imports, produce TSX + JSX
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Convert PascalCase icon component name to kebab-case icon name. */
|
||||
function componentToIconName(name: string): string {
|
||||
return name
|
||||
.replace(/Icon$/, '')
|
||||
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/** Convert HTML SVG attribute names to JSX camelCase equivalents. */
|
||||
function svgToJsx(svg: string): string {
|
||||
return svg
|
||||
.replace(/\bstroke-width=/g, 'strokeWidth=')
|
||||
.replace(/\bstroke-linecap=/g, 'strokeLinecap=')
|
||||
.replace(/\bstroke-linejoin=/g, 'strokeLinejoin=')
|
||||
.replace(/\bstroke-dasharray=/g, 'strokeDasharray=')
|
||||
.replace(/\bstroke-dashoffset=/g, 'strokeDashoffset=')
|
||||
.replace(/\bstroke-miterlimit=/g, 'strokeMiterlimit=')
|
||||
.replace(/\bfill-rule=/g, 'fillRule=')
|
||||
.replace(/\bclip-rule=/g, 'clipRule=')
|
||||
.replace(/\bfill-opacity=/g, 'fillOpacity=')
|
||||
.replace(/\bstroke-opacity=/g, 'strokeOpacity=');
|
||||
}
|
||||
|
||||
/** Load the raw icons map from a render dist module. */
|
||||
async function loadIconsMap(iconSet: 'default' | 'minimal'): Promise<Record<string, string>> {
|
||||
const mod = await import(pkgDistUrl(`@videojs/icons/render/${iconSet}`));
|
||||
const renderIcon = mod.renderIcon as (name: string) => string;
|
||||
const assetsDir = resolve(PACKAGES_ROOT, 'icons/src/assets', iconSet);
|
||||
const iconNames = readdirSync(assetsDir)
|
||||
.filter((f) => f.endsWith('.svg'))
|
||||
.map((f) => f.replace(/\.svg$/, ''));
|
||||
const map: Record<string, string> = {};
|
||||
for (const name of iconNames) {
|
||||
const svg = renderIcon(name);
|
||||
if (svg) map[name] = svg;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Serialize a JS value to source code. */
|
||||
function serializeValue(value: unknown, indent = 0): string {
|
||||
if (typeof value === 'string') return JSON.stringify(value);
|
||||
@@ -1019,46 +988,18 @@ function splitTopLevelCommas(str: string): string[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Remove icon imports and generate icon component definitions. */
|
||||
async function inlineReactIcons(source: string): Promise<{ source: string; iconComponents: string[] }> {
|
||||
const sourceFile = createSourceFile('react-skin.tsx', source);
|
||||
const iconImport = sourceFile.statements.find((statement) => {
|
||||
if (!ts.isImportDeclaration(statement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const specifier = statement.moduleSpecifier.getText(sourceFile).slice(1, -1);
|
||||
return /^@videojs\/icons\/react(?:\/(default|minimal))?$/.test(specifier);
|
||||
});
|
||||
|
||||
if (!iconImport || !ts.isImportDeclaration(iconImport)) {
|
||||
return { source, iconComponents: [] };
|
||||
}
|
||||
|
||||
const iconSpecifier = iconImport.moduleSpecifier.getText(sourceFile).slice(1, -1);
|
||||
const iconSetMatch = iconSpecifier.match(/@videojs\/icons\/react(?:\/(default|minimal))?/);
|
||||
const iconSet = (iconSetMatch?.[1] || 'default') as 'default' | 'minimal';
|
||||
const namedBindings = iconImport.importClause?.namedBindings;
|
||||
const iconNames =
|
||||
namedBindings && ts.isNamedImports(namedBindings) ? namedBindings.elements.map((element) => element.name.text) : [];
|
||||
|
||||
const iconsMap = await loadIconsMap(iconSet);
|
||||
const iconComponents: string[] = [];
|
||||
|
||||
for (const componentName of iconNames) {
|
||||
const iconName = componentToIconName(componentName);
|
||||
const rawSvg = iconsMap[iconName];
|
||||
if (!rawSvg) {
|
||||
log.warn(`No SVG found for ${componentName} (icon: ${iconName})`);
|
||||
continue;
|
||||
}
|
||||
const jsxSvg = svgToJsx(rawSvg).replace(/^(<svg[^>]*)>/, '$1 {...props}>');
|
||||
iconComponents.push(`function ${componentName}(props: ComponentProps<'svg'>): ReactNode {\n return ${jsxSvg};\n}`);
|
||||
}
|
||||
|
||||
// Remove the icon import, keep JSX component calls as-is
|
||||
source = `${source.slice(0, iconImport.getFullStart())}${source.slice(iconImport.getEnd())}`;
|
||||
return { source, iconComponents };
|
||||
/**
|
||||
* Rewrite package-private or local React icon imports to public package
|
||||
* re-exports for ejected skins.
|
||||
*/
|
||||
function rewriteReactIconImports(source: string): string {
|
||||
return source
|
||||
.replace(/from\s+['"]@videojs\/icons\/react(?:\/default)?['"]/g, "from '@videojs/react/icons'")
|
||||
.replace(/from\s+['"]@videojs\/icons\/react\/minimal['"]/g, "from '@videojs/react/icons/minimal'")
|
||||
.replace(/from\s+['"]@\/icons['"]/g, "from '@videojs/react/icons'")
|
||||
.replace(/from\s+['"]@\/icons\/minimal['"]/g, "from '@videojs/react/icons/minimal'")
|
||||
.replace(/from\s+['"]\.\.\/\.\.\/icons['"]/g, "from '@videojs/react/icons'")
|
||||
.replace(/from\s+['"]\.\.\/\.\.\/icons\/minimal['"]/g, "from '@videojs/react/icons/minimal'");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1535,55 +1476,52 @@ function flattenSkinIntoPlayer(source: string, mediaType: MediaType): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a React skin: inline SVG icons, resolve all imports,
|
||||
* Process a React skin: rewrite icon imports, resolve imports,
|
||||
* and produce both TSX and JSX versions.
|
||||
*/
|
||||
async function processReactSkin(skin: ReactSkinDef): Promise<{ tsx: string; jsx: string }> {
|
||||
const absPath = resolve(ROOT, skin.source);
|
||||
let source = readFileSync(absPath, 'utf-8');
|
||||
source = rewriteReactIconImports(source);
|
||||
validatePackageImports(source, skin.source);
|
||||
const postImport: string[] = [];
|
||||
|
||||
// 1. Inline relative imports recursively so the output is self-contained.
|
||||
source = inlineRelativeImports(source, absPath);
|
||||
source = inlineRelativeImports(source, absPath, rewriteReactIconImports);
|
||||
|
||||
// 2. Extract icon components (remove import, keep JSX calls, generate components)
|
||||
const icons = await inlineReactIcons(source);
|
||||
source = icons.source;
|
||||
|
||||
// 3. Resolve @videojs/skins/* tokens (Tailwind skins only, private package)
|
||||
// 2. Resolve @videojs/skins/* tokens (Tailwind skins only, private package)
|
||||
source = await inlineSkinTokens(source, postImport);
|
||||
|
||||
// 4. Replace cn calls with template literals
|
||||
// 3. Replace cn calls with template literals
|
||||
source = inlineCn(source);
|
||||
|
||||
// 5. Consolidate @/ path aliases → @videojs/react
|
||||
// 4. Consolidate @/ path aliases → @videojs/react
|
||||
source = rewritePathAliases(source);
|
||||
|
||||
// 6. Inline private package imports (core/dom → react, predicates, isRenderProp)
|
||||
// 5. Inline private package imports (core/dom → react, predicates, isRenderProp)
|
||||
const privates = inlinePrivatePackages(source);
|
||||
source = privates.source;
|
||||
|
||||
// 7. Insert collected non-import code after the final import statement
|
||||
// 6. Insert collected non-import code after the final import statement
|
||||
if (postImport.length > 0) {
|
||||
const insertPos = findLastImportEnd(source);
|
||||
const block = `\n${postImport.join('\n\n')}\n`;
|
||||
source = `${source.slice(0, insertPos)}${block}${source.slice(insertPos)}`;
|
||||
}
|
||||
|
||||
// 8. Replace Base*SkinProps chain with a clean interface
|
||||
// 7. Replace Base*SkinProps chain with a clean interface
|
||||
source = resolvePropsInterface(source);
|
||||
|
||||
// 9. Flatten ERROR_CLASSNAMES into ErrorDialog JSX (@temporary — remove with flattenErrorClasses)
|
||||
// 8. Flatten ERROR_CLASSNAMES into ErrorDialog JSX (@temporary — remove with flattenErrorClasses)
|
||||
source = flattenErrorClasses(source);
|
||||
|
||||
// 10. Reorganize into sections with comment headers
|
||||
let tsx = reorganizeReactOutput(source, privates.utilities, icons.iconComponents);
|
||||
// 9. Reorganize into sections with comment headers
|
||||
let tsx = reorganizeReactOutput(source, privates.utilities, []);
|
||||
|
||||
// 11. Destructure skin props in function argument instead of body
|
||||
// 10. Destructure skin props in function argument instead of body
|
||||
tsx = destructureSkinProps(tsx);
|
||||
|
||||
// 12. Flatten skin into player (merge props, inline body, wrap in Player.Provider)
|
||||
// 11. Flatten skin into player (merge props, inline body, wrap in Player.Provider)
|
||||
tsx = flattenSkinIntoPlayer(tsx, getSkinMediaType(skin));
|
||||
|
||||
const jsx = tsxToJsx(tsx);
|
||||
|
||||
Reference in New Issue
Block a user