Skip to content

Commit 78de97e

Browse files
authored
feat(icons): setup icons package (#536)
1 parent edefc2a commit 78de97e

File tree

35 files changed

+504
-14
lines changed

35 files changed

+504
-14
lines changed

.claude/skills/api-reference/references/demo-patterns.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import '@videojs/html/ui/mute-button';
109109
```
110110

111111
Import registration for:
112+
112113
- `@videojs/html/video/player` — always needed (registers `<video-player>`)
113114
- `@videojs/html/ui/{component}` — registers the component's custom element
114115

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

119120
```tsx
120-
import { createPlayer, features, MuteButton, Video } from '@videojs/react';
121+
import { createPlayer, features, MuteButton } from '@videojs/react';
122+
import { Video } from '@videojs/react/video';
121123

122124
import './BasicUsage.css';
123125

@@ -147,6 +149,7 @@ export default function BasicUsage() {
147149
```
148150

149151
Key patterns:
152+
150153
- `createPlayer({ features: [...features.video] })` creates the player
151154
- Video attributes: `autoPlay muted playsInline loop` (React camelCase)
152155
- `render` prop for state-based rendering: `render={(props, state) => ...}`

.claude/skills/docs/patterns/code-examples.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ function App() {
2424
}
2525

2626
// ✅ Complete — copy, paste, run
27-
import { createPlayer, features, PlayButton, Video } from '@videojs/react';
27+
import { createPlayer, features, PlayButton } from '@videojs/react';
28+
import { Video } from '@videojs/react/video';
2829

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

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

7980
```tsx
80-
import { createPlayer, features, PlayButton, Video } from '@videojs/react';
81+
import { createPlayer, features, PlayButton } from '@videojs/react';
82+
import { Video } from '@videojs/react/video';
8183

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

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

223225
````markdown
224226
```tsx title="App.tsx"
225-
import { createPlayer, features, PlayButton, Video } from '@videojs/react';
227+
import { createPlayer, features, PlayButton } from '@videojs/react';
228+
import { Video } from '@videojs/react/video';
226229
import './App.css';
227230

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

packages/icons/package.json

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,55 @@
11
{
22
"name": "@videojs/icons",
33
"type": "module",
4+
"private": true,
45
"version": "0.1.0-alpha.1",
56
"description": "SVG icon library for Video.js",
67
"license": "Apache-2.0",
7-
"files": [],
8+
"sideEffects": false,
9+
"exports": {
10+
"./react": {
11+
"types": "./dist/react/default/index.d.ts",
12+
"default": "./dist/react/default/index.js"
13+
},
14+
"./react/*": {
15+
"types": "./dist/react/*/index.d.ts",
16+
"default": "./dist/react/*/index.js"
17+
},
18+
"./html": {
19+
"types": "./dist/html/default/index.d.ts",
20+
"default": "./dist/html/default/index.js"
21+
},
22+
"./html/*": {
23+
"types": "./dist/html/*/index.d.ts",
24+
"default": "./dist/html/*/index.js"
25+
}
26+
},
27+
"files": [
28+
"dist"
29+
],
30+
"scripts": {
31+
"build": "node --import tsx scripts/build.ts",
32+
"clean": "rm -rf dist"
33+
},
34+
"dependencies": {
35+
"svgo": "^3.3.2"
36+
},
837
"devDependencies": {
9-
"tsdown": "^0.20.3",
38+
"@svgr/core": "^8.1.0",
39+
"@svgr/plugin-jsx": "^8.1.0",
40+
"@svgr/plugin-svgo": "^8.1.0",
41+
"@types/react": "^19.0.0",
42+
"@videojs/utils": "workspace:*",
43+
"react": "^19.0.0",
44+
"tsx": "^4.19.0",
1045
"typescript": "^5.9.3"
1146
},
1247
"publishConfig": {
1348
"access": "public"
14-
}
49+
},
50+
"keywords": [
51+
"videojs",
52+
"icons",
53+
"svg"
54+
]
1555
}

packages/icons/scripts/build.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2+
import { dirname, join } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { transform } from '@svgr/core';
5+
import { camelCase, pascalCase } from '@videojs/utils/string';
6+
import { transform as esbuildTransform } from 'esbuild';
7+
import { optimize } from 'svgo';
8+
9+
const __dirname = dirname(fileURLToPath(import.meta.url));
10+
const ROOT = join(__dirname, '..');
11+
const ASSETS_DIR = join(ROOT, 'src/assets');
12+
const DIST_DIR = join(ROOT, 'dist');
13+
14+
const FRAMEWORKS = ['react', 'html'] as const;
15+
type Framework = (typeof FRAMEWORKS)[number];
16+
17+
const SVGO_CONFIG = {
18+
multipass: true,
19+
plugins: [],
20+
};
21+
22+
function ensureDir(path: string): void {
23+
if (!existsSync(path)) mkdirSync(path, { recursive: true });
24+
}
25+
26+
function cleanDist(): void {
27+
if (existsSync(DIST_DIR)) rmSync(DIST_DIR, { recursive: true, force: true });
28+
}
29+
30+
function getIconSets(): string[] {
31+
if (!existsSync(ASSETS_DIR)) {
32+
console.error(`Assets directory not found: ${ASSETS_DIR}`);
33+
process.exit(1);
34+
}
35+
return readdirSync(ASSETS_DIR).filter((item) => !item.startsWith('.') && item !== 'index');
36+
}
37+
38+
function getSvgFiles(setName: string): string[] {
39+
return readdirSync(join(ASSETS_DIR, setName)).filter((f) => f.endsWith('.svg'));
40+
}
41+
42+
function optimizeSvg(svgContent: string): string {
43+
return optimize(svgContent, SVGO_CONFIG).data;
44+
}
45+
46+
async function buildReactComponent(svgContent: string, componentName: string): Promise<{ js: string; tsx: string }> {
47+
const transformOpts = {
48+
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
49+
svgoConfig: SVGO_CONFIG,
50+
};
51+
52+
const tsxCode = await transform(svgContent, { ...transformOpts, typescript: true }, { componentName });
53+
const jsxCode = await transform(svgContent, transformOpts, { componentName });
54+
55+
// SVGR outputs JSX syntax which is invalid in .js files — compile to JS
56+
const { code } = await esbuildTransform(jsxCode, { loader: 'jsx', jsx: 'automatic' });
57+
58+
return { js: code, tsx: tsxCode };
59+
}
60+
61+
function buildHtmlExport(svgContent: string, varName: string): string {
62+
return `export const ${varName} = \`${optimizeSvg(svgContent)}\`;\n`;
63+
}
64+
65+
function buildIndexExports(icons: { name: string; varName: string }[], framework: Framework): string {
66+
return icons
67+
.map(({ name, varName }) =>
68+
framework === 'react'
69+
? `export { default as ${pascalCase(varName)}Icon } from './${name}.js';`
70+
: `export { ${camelCase(varName)}Icon } from './${name}.js';`
71+
)
72+
.join('\n');
73+
}
74+
75+
function buildIndexTypes(icons: { name: string; varName: string }[], framework: Framework): string {
76+
const types = icons.map(({ varName }) =>
77+
framework === 'react'
78+
? `export declare const ${pascalCase(varName)}Icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;`
79+
: `export declare const ${camelCase(varName)}Icon: string;`
80+
);
81+
return `/// <reference types="react" />\n${types.join('\n')}\n`;
82+
}
83+
84+
async function buildIconSet(setName: string): Promise<void> {
85+
const svgFiles = getSvgFiles(setName);
86+
console.log(` Building set: ${setName} (${svgFiles.length} icons)`);
87+
88+
const icons = svgFiles.map((file) => ({
89+
name: file.replace('.svg', ''),
90+
varName: file.replace('.svg', ''),
91+
content: readFileSync(join(ASSETS_DIR, setName, file), 'utf8'),
92+
}));
93+
94+
for (const framework of FRAMEWORKS) {
95+
const outDir = join(DIST_DIR, framework, setName);
96+
ensureDir(outDir);
97+
98+
for (const icon of icons) {
99+
const { name, varName, content } = icon;
100+
101+
if (framework === 'react') {
102+
const componentName = `${pascalCase(varName)}Icon`;
103+
const { js, tsx } = await buildReactComponent(content, componentName);
104+
writeFileSync(join(outDir, `${name}.js`), js);
105+
writeFileSync(join(outDir, `${name}.tsx`), tsx);
106+
writeFileSync(
107+
join(outDir, `${name}.d.ts`),
108+
`import * as React from 'react';\ndeclare const ${componentName}: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;\nexport default ${componentName};\n`
109+
);
110+
} else {
111+
const varNameCamel = camelCase(varName);
112+
writeFileSync(join(outDir, `${name}.js`), buildHtmlExport(content, `${varNameCamel}Icon`));
113+
writeFileSync(join(outDir, `${name}.d.ts`), `export declare const ${varNameCamel}Icon: string;\n`);
114+
}
115+
}
116+
117+
writeFileSync(join(outDir, 'index.js'), buildIndexExports(icons, framework));
118+
writeFileSync(join(outDir, 'index.d.ts'), buildIndexTypes(icons, framework));
119+
}
120+
}
121+
122+
async function main(): Promise<void> {
123+
console.log('Building icons...\n');
124+
cleanDist();
125+
126+
const sets = getIconSets();
127+
console.log(`Found ${sets.length} icon sets: ${sets.join(', ')}\n`);
128+
129+
for (const set of sets) {
130+
await buildIconSet(set);
131+
}
132+
133+
console.log('\nBuild complete!');
134+
}
135+
136+
main().catch(console.error);
Lines changed: 10 additions & 0 deletions
Loading
Lines changed: 10 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 35 additions & 0 deletions
Loading
Lines changed: 8 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)