Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .claude/skills/api-reference/references/demo-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import '@videojs/html/ui/mute-button';
```

Import registration for:

- `@videojs/html/video/player` — always needed (registers `<video-player>`)
- `@videojs/html/ui/{component}` — registers the component's custom element

Expand All @@ -117,7 +118,8 @@ Import registration for:
### .tsx (component)

```tsx
import { createPlayer, features, MuteButton, Video } from '@videojs/react';
import { createPlayer, features, MuteButton } from '@videojs/react';
import { Video } from '@videojs/react/video';

import './BasicUsage.css';

Expand Down Expand Up @@ -147,6 +149,7 @@ export default function BasicUsage() {
```

Key patterns:

- `createPlayer({ features: [...features.video] })` creates the player
- Video attributes: `autoPlay muted playsInline loop` (React camelCase)
- `render` prop for state-based rendering: `render={(props, state) => ...}`
Expand Down
9 changes: 6 additions & 3 deletions .claude/skills/docs/patterns/code-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ function App() {
}

// ✅ Complete — copy, paste, run
import { createPlayer, features, PlayButton, Video } from '@videojs/react';
import { createPlayer, features, PlayButton } from '@videojs/react';
import { Video } from '@videojs/react/video';

const Player = createPlayer({ features: [...features.video] });

Expand Down Expand Up @@ -77,7 +78,8 @@ Site pages use `<FrameworkCase>` and `<StyleCase>` to show code per framework. N
**React:**

```tsx
import { createPlayer, features, PlayButton, Video } from '@videojs/react';
import { createPlayer, features, PlayButton } from '@videojs/react';
import { Video } from '@videojs/react/video';

const Player = createPlayer({ features: [...features.video] });

Expand Down Expand Up @@ -222,7 +224,8 @@ Show which file code belongs to when multiple files are involved:

````markdown
```tsx title="App.tsx"
import { createPlayer, features, PlayButton, Video } from '@videojs/react';
import { createPlayer, features, PlayButton } from '@videojs/react';
import { Video } from '@videojs/react/video';
import './App.css';

const Player = createPlayer({ features: [...features.video] });
Expand Down
46 changes: 43 additions & 3 deletions packages/icons/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
{
"name": "@videojs/icons",
"type": "module",
"private": true,
"version": "0.1.0-alpha.1",
"description": "SVG icon library for Video.js",
"license": "Apache-2.0",
"files": [],
"sideEffects": false,
"exports": {
"./react": {
"types": "./dist/react/default/index.d.ts",
"default": "./dist/react/default/index.js"
},
"./react/*": {
"types": "./dist/react/*/index.d.ts",
"default": "./dist/react/*/index.js"
},
"./html": {
"types": "./dist/html/default/index.d.ts",
"default": "./dist/html/default/index.js"
},
"./html/*": {
"types": "./dist/html/*/index.d.ts",
"default": "./dist/html/*/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "node --import tsx scripts/build.ts",
"clean": "rm -rf dist"
},
"dependencies": {
"svgo": "^3.3.2"
},
"devDependencies": {
"tsdown": "^0.20.3",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"@types/react": "^19.0.0",
"@videojs/utils": "workspace:*",
"react": "^19.0.0",
"tsx": "^4.19.0",
"typescript": "^5.9.3"
},
"publishConfig": {
"access": "public"
}
},
"keywords": [
"videojs",
"icons",
"svg"
]
}
136 changes: 136 additions & 0 deletions packages/icons/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { transform } from '@svgr/core';
import { camelCase, pascalCase } from '@videojs/utils/string';
import { transform as esbuildTransform } from 'esbuild';
import { optimize } from 'svgo';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const ASSETS_DIR = join(ROOT, 'src/assets');
const DIST_DIR = join(ROOT, 'dist');

const FRAMEWORKS = ['react', 'html'] as const;
type Framework = (typeof FRAMEWORKS)[number];

const SVGO_CONFIG = {
multipass: true,
plugins: [],
};

function ensureDir(path: string): void {
if (!existsSync(path)) mkdirSync(path, { recursive: true });
}

function cleanDist(): void {
if (existsSync(DIST_DIR)) rmSync(DIST_DIR, { recursive: true, force: true });
}

function getIconSets(): string[] {
if (!existsSync(ASSETS_DIR)) {
console.error(`Assets directory not found: ${ASSETS_DIR}`);
process.exit(1);
}
return readdirSync(ASSETS_DIR).filter((item) => !item.startsWith('.') && item !== 'index');
}

function getSvgFiles(setName: string): string[] {
return readdirSync(join(ASSETS_DIR, setName)).filter((f) => f.endsWith('.svg'));
}

function optimizeSvg(svgContent: string): string {
return optimize(svgContent, SVGO_CONFIG).data;
}

async function buildReactComponent(svgContent: string, componentName: string): Promise<{ js: string; tsx: string }> {
const transformOpts = {
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
svgoConfig: SVGO_CONFIG,
};

const tsxCode = await transform(svgContent, { ...transformOpts, typescript: true }, { componentName });
const jsxCode = await transform(svgContent, transformOpts, { componentName });

// SVGR outputs JSX syntax which is invalid in .js files — compile to JS
const { code } = await esbuildTransform(jsxCode, { loader: 'jsx', jsx: 'automatic' });

return { js: code, tsx: tsxCode };
}

function buildHtmlExport(svgContent: string, varName: string): string {
return `export const ${varName} = \`${optimizeSvg(svgContent)}\`;\n`;
}

function buildIndexExports(icons: { name: string; varName: string }[], framework: Framework): string {
return icons
.map(({ name, varName }) =>
framework === 'react'
? `export { default as ${pascalCase(varName)}Icon } from './${name}.js';`
: `export { ${camelCase(varName)}Icon } from './${name}.js';`
)
.join('\n');
}

function buildIndexTypes(icons: { name: string; varName: string }[], framework: Framework): string {
const types = icons.map(({ varName }) =>
framework === 'react'
? `export declare const ${pascalCase(varName)}Icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;`
: `export declare const ${camelCase(varName)}Icon: string;`
);
return `/// <reference types="react" />\n${types.join('\n')}\n`;
}

async function buildIconSet(setName: string): Promise<void> {
const svgFiles = getSvgFiles(setName);
console.log(` Building set: ${setName} (${svgFiles.length} icons)`);

const icons = svgFiles.map((file) => ({
name: file.replace('.svg', ''),
varName: file.replace('.svg', ''),
content: readFileSync(join(ASSETS_DIR, setName, file), 'utf8'),
}));

for (const framework of FRAMEWORKS) {
const outDir = join(DIST_DIR, framework, setName);
ensureDir(outDir);

for (const icon of icons) {
const { name, varName, content } = icon;

if (framework === 'react') {
const componentName = `${pascalCase(varName)}Icon`;
const { js, tsx } = await buildReactComponent(content, componentName);
writeFileSync(join(outDir, `${name}.js`), js);
writeFileSync(join(outDir, `${name}.tsx`), tsx);
writeFileSync(
join(outDir, `${name}.d.ts`),
`import * as React from 'react';\ndeclare const ${componentName}: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;\nexport default ${componentName};\n`
);
} else {
const varNameCamel = camelCase(varName);
writeFileSync(join(outDir, `${name}.js`), buildHtmlExport(content, `${varNameCamel}Icon`));
writeFileSync(join(outDir, `${name}.d.ts`), `export declare const ${varNameCamel}Icon: string;\n`);
}
}

writeFileSync(join(outDir, 'index.js'), buildIndexExports(icons, framework));
writeFileSync(join(outDir, 'index.d.ts'), buildIndexTypes(icons, framework));
}
}

async function main(): Promise<void> {
console.log('Building icons...\n');
cleanDist();

const sets = getIconSets();
console.log(`Found ${sets.length} icon sets: ${sets.join(', ')}\n`);

for (const set of sets) {
await buildIconSet(set);
}

console.log('\nBuild complete!');
}

main().catch(console.error);
10 changes: 10 additions & 0 deletions packages/icons/src/assets/default/fullscreen-enter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/icons/src/assets/default/fullscreen-exit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/icons/src/assets/default/pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/icons/src/assets/default/play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions packages/icons/src/assets/default/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/icons/src/assets/default/volume-high.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/icons/src/assets/default/volume-low.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/icons/src/assets/default/volume-off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/icons/src/assets/minimal/fullscreen-enter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/icons/src/assets/minimal/fullscreen-exit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/icons/src/assets/minimal/pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions packages/icons/src/assets/minimal/play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading