mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add UI support for gestures and hotkeys (#1388)
Co-authored-by: Rahim <rahim.alwer@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Rahim
Claude Opus 4.6
parent
6c81f2d190
commit
0620814a67
@@ -1,48 +1,39 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, watch, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, watch, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const isWatch = process.argv.includes('--watch');
|
||||
|
||||
import { transform } from '@svgr/core';
|
||||
import { camelCase, pascalCase } from '@videojs/utils/string';
|
||||
import { transform as esbuildTransform } from 'esbuild';
|
||||
import { type Config, optimize } from 'svgo';
|
||||
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');
|
||||
import {
|
||||
ASSETS_DIR,
|
||||
createSvgoConfig,
|
||||
DIST_DIR,
|
||||
getIconSets,
|
||||
getSvgFiles,
|
||||
PRESET_DEFAULT_OVERRIDES,
|
||||
REMOVE_ATTRS_PLUGIN,
|
||||
replaceColors,
|
||||
} from './shared.js';
|
||||
|
||||
const FRAMEWORKS = ['react', 'html'] as const;
|
||||
|
||||
const SVGO_CONFIG: Config = {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
convertColors: {
|
||||
currentColor: /^black$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
const SVGO_CONFIG = createSvgoConfig([
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: { overrides: PRESET_DEFAULT_OVERRIDES },
|
||||
},
|
||||
REMOVE_ATTRS_PLUGIN,
|
||||
{
|
||||
name: 'addAttributesToSVGElement',
|
||||
params: {
|
||||
attributes: [{ 'aria-hidden': 'true' }],
|
||||
},
|
||||
{
|
||||
name: 'removeAttrs',
|
||||
params: {
|
||||
attrs: ['^clip-rule$', '^fill-rule$'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'addAttributesToSVGElement',
|
||||
params: {
|
||||
attributes: [{ 'aria-hidden': 'true' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
||||
function ensureDir(path: string): void {
|
||||
if (!existsSync(path)) mkdirSync(path, { recursive: true });
|
||||
@@ -52,36 +43,21 @@ 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 {
|
||||
const optimized = optimize(svgContent, SVGO_CONFIG).data;
|
||||
return optimized
|
||||
.replaceAll('fill="black"', 'fill="currentColor"')
|
||||
.replaceAll('stroke="black"', 'stroke="currentColor"');
|
||||
return replaceColors(optimize(svgContent, SVGO_CONFIG).data);
|
||||
}
|
||||
|
||||
async function buildReactComponent(svgContent: string, componentName: string): Promise<{ js: string; tsx: string }> {
|
||||
const optimized = optimizeSvg(svgContent);
|
||||
|
||||
const transformOpts: Parameters<typeof transform>[1] = {
|
||||
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
|
||||
plugins: ['@svgr/plugin-jsx'],
|
||||
jsxRuntime: 'automatic',
|
||||
svgoConfig: SVGO_CONFIG,
|
||||
};
|
||||
|
||||
const tsxCode = await transform(svgContent, { ...transformOpts, typescript: true }, { componentName });
|
||||
const jsxCode = await transform(svgContent, transformOpts, { componentName });
|
||||
const tsxCode = await transform(optimized, { ...transformOpts, typescript: true }, { componentName });
|
||||
const jsxCode = await transform(optimized, 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 };
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import type { CustomPlugin, XastElement } from 'svgo';
|
||||
import { optimize } from 'svgo';
|
||||
|
||||
import {
|
||||
ASSETS_DIR,
|
||||
createSvgoConfig,
|
||||
getIconSets,
|
||||
getSvgFiles,
|
||||
PRESET_DEFAULT_OVERRIDES,
|
||||
REMOVE_ATTRS_PLUGIN,
|
||||
replaceColors,
|
||||
} from './shared.js';
|
||||
|
||||
const SHAPES = new Set(['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect']);
|
||||
|
||||
function allShapesUseCurrentColor(node: XastElement, inheritedFill: string): boolean {
|
||||
for (const child of node.children) {
|
||||
if (child.type !== 'element') continue;
|
||||
|
||||
const effectiveFill = child.attributes.fill ?? inheritedFill;
|
||||
|
||||
if (SHAPES.has(child.name)) {
|
||||
if (effectiveFill !== 'currentColor') return false;
|
||||
} else if (!allShapesUseCurrentColor(child, effectiveFill)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasShapeDescendant(node: XastElement): boolean {
|
||||
for (const child of node.children) {
|
||||
if (child.type !== 'element') continue;
|
||||
if (SHAPES.has(child.name) || hasShapeDescendant(child)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeFillCurrentColor(node: XastElement): void {
|
||||
if (node.attributes.fill === 'currentColor') {
|
||||
delete node.attributes.fill;
|
||||
}
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'element') removeFillCurrentColor(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root `<svg>` has `fill="none"` but every shape descendant uses
|
||||
* `fill="currentColor"` (directly or inherited from a `<g>`), hoist
|
||||
* `fill="currentColor"` to the root and strip it from descendants.
|
||||
*
|
||||
* With `multipass: true`, SVGO's `collapseGroups` will then clean up any
|
||||
* `<g>` elements left with no attributes on the next pass.
|
||||
*/
|
||||
const hoistCurrentColorFill: CustomPlugin = {
|
||||
name: 'hoistCurrentColorFill',
|
||||
fn: () => ({
|
||||
element: {
|
||||
exit(node) {
|
||||
if (node.name !== 'svg') return;
|
||||
if (node.attributes.fill !== 'none') return;
|
||||
if (!hasShapeDescendant(node)) return;
|
||||
if (!allShapesUseCurrentColor(node, 'none')) return;
|
||||
|
||||
node.attributes.fill = 'currentColor';
|
||||
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'element') removeFillCurrentColor(child);
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const SVGO_CONFIG = createSvgoConfig(
|
||||
[
|
||||
{
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
...PRESET_DEFAULT_OVERRIDES,
|
||||
convertShapeToPath: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
REMOVE_ATTRS_PLUGIN,
|
||||
hoistCurrentColorFill,
|
||||
],
|
||||
{
|
||||
js2svg: {
|
||||
indent: 2,
|
||||
pretty: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function formatFile(filePath: string): boolean {
|
||||
const input = readFileSync(filePath, 'utf8');
|
||||
const formatted = replaceColors(optimize(input, SVGO_CONFIG).data);
|
||||
|
||||
if (formatted !== input) {
|
||||
writeFileSync(filePath, formatted);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getAllSvgFiles(): string[] {
|
||||
return getIconSets().flatMap((set) => getSvgFiles(set).map((file) => join(ASSETS_DIR, set, file)));
|
||||
}
|
||||
|
||||
const files = process.argv.length > 2 ? process.argv.slice(2) : getAllSvgFiles();
|
||||
|
||||
let changed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (formatFile(file)) {
|
||||
console.log(` formatted: ${file}`);
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed > 0) {
|
||||
console.log(`\nFormatted ${changed} of ${files.length} SVG files.`);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import type { Config, PluginConfig } from 'svgo';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const ROOT = join(__dirname, '..');
|
||||
export const ASSETS_DIR = join(ROOT, 'src/assets');
|
||||
export const DIST_DIR = join(ROOT, 'dist');
|
||||
|
||||
export const PRESET_DEFAULT_OVERRIDES = {
|
||||
convertColors: {
|
||||
currentColor: /^black$/,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const REMOVE_ATTRS_PLUGIN: PluginConfig = {
|
||||
name: 'removeAttrs',
|
||||
params: {
|
||||
attrs: ['^clip-rule$', '^fill-rule$'],
|
||||
},
|
||||
};
|
||||
|
||||
export function createSvgoConfig(plugins: PluginConfig[], options?: Omit<Config, 'plugins'>): Config {
|
||||
return { multipass: true, ...options, plugins };
|
||||
}
|
||||
|
||||
export function replaceColors(svg: string): string {
|
||||
return svg.replaceAll('fill="black"', 'fill="currentColor"').replaceAll('stroke="black"', 'stroke="currentColor"');
|
||||
}
|
||||
|
||||
export 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');
|
||||
}
|
||||
|
||||
export function getSvgFiles(setName: string): string[] {
|
||||
return readdirSync(join(ASSETS_DIR, setName)).filter((f) => f.endsWith('.svg'));
|
||||
}
|
||||
Reference in New Issue
Block a user