From 7c412d7be99b5cc3c90c423bce8aa294f4cb6b96 Mon Sep 17 00:00:00 2001 From: Daria Larionova Date: Thu, 16 Jan 2025 15:22:50 +0300 Subject: [PATCH] chore: add dev scripts --- .gitignore | 1 + README-ru.md | 8 +++ README.md | 8 +++ package-lock.json | 10 ++- package.json | 5 +- scripts/constants.mjs | 24 +++++++ .../generate-components-from-templates.mjs | 59 ++++++++++++++++++ scripts/generate-svgs-from-templates.mjs | 40 ++++++++++++ scripts/generate-templates.mjs | 37 +++++++++++ scripts/gravity-colors-config.mjs | 47 ++++++++++++++ scripts/transform-colors.mjs | 62 +++++++++++++++++++ scripts/utils.mjs | 13 ++++ 12 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 scripts/constants.mjs create mode 100644 scripts/generate-components-from-templates.mjs create mode 100644 scripts/generate-svgs-from-templates.mjs create mode 100644 scripts/generate-templates.mjs create mode 100644 scripts/gravity-colors-config.mjs create mode 100644 scripts/transform-colors.mjs create mode 100644 scripts/utils.mjs diff --git a/.gitignore b/.gitignore index fdd760c..2ba0777 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules /*.js !/.*.js storybook-static +templates \ No newline at end of file diff --git a/README-ru.md b/README-ru.md index 8f33b42..317319b 100644 --- a/README-ru.md +++ b/README-ru.md @@ -85,3 +85,11 @@ import {NotFound} from '@gravity-ui/illustrations'; ```js import notFound from '@gravity-ui/illustrations/svgs/not-found-light.svg'; ``` + +### Разработка + +Для обновления иллюстраций в соответствии с новым дизайном, измените контент svg-файлов в светлой теме (`/svgs/-light.svg` файлы) и затем запустите команду: + +```shell +npm run generate +``` diff --git a/README.md b/README.md index b389537..1ce1e2c 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,11 @@ import {NotFound} from '@gravity-ui/illustrations'; ```js import notFound from '@gravity-ui/illustrations/svgs/not-found-light.svg'; ``` + +### Development + +For updating illustrations according to new design, change the content of svg-s in light theme (`/svgs/-light.svg` files) and then run command: + +```shell +npm run generate +``` diff --git a/package-lock.json b/package-lock.json index bcb9f1d..1e1b3a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@storybook/react": "^8.0.2", "@storybook/react-webpack5": "^8.0.2", "@storybook/theming": "^8.0.2", + "@svgr/core": "^8.1.0", "@svgr/webpack": "^8.1.0", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", @@ -39,6 +40,7 @@ "storybook": "^8.0.2", "style-loader": "^3.3.4", "stylelint": "^15.11.0", + "svgo": "^3.3.2", "typescript": "^5.4.2" }, "peerDependencies": { @@ -5592,6 +5594,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -16385,10 +16388,11 @@ "dev": true }, "node_modules/svgo": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz", - "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, + "license": "MIT", "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", diff --git a/package.json b/package.json index 1e460ad..b5be6df 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "build": "tsc", "start": "TS_NODE_PROJECT=.storybook/tsconfig.json storybook dev", "prepublishOnly": "npm run build", - "optimize": "npx svgo svgs/*" + "optimize": "npx svgo -f svgs --multipass", + "generate": "node scripts/generate-templates.mjs && node scripts/generate-components-from-templates.mjs && node scripts/generate-svgs-from-templates.mjs" }, "devDependencies": { "@bem-react/classname": "^1.6.0", @@ -46,6 +47,7 @@ "@storybook/react": "^8.0.2", "@storybook/react-webpack5": "^8.0.2", "@storybook/theming": "^8.0.2", + "@svgr/core": "^8.1.0", "@svgr/webpack": "^8.1.0", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", @@ -61,6 +63,7 @@ "storybook": "^8.0.2", "style-loader": "^3.3.4", "stylelint": "^15.11.0", + "svgo": "^3.3.2", "typescript": "^5.4.2" }, "peerDependencies": { diff --git a/scripts/constants.mjs b/scripts/constants.mjs new file mode 100644 index 0000000..3452cb6 --- /dev/null +++ b/scripts/constants.mjs @@ -0,0 +1,24 @@ +import path from 'path'; +import url from 'url'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +export const SVGS_DIR = path.resolve(__dirname, '..', 'svgs'); +export const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates'); +export const ICONS_DIR = path.resolve(__dirname, '..', 'src', 'components'); +export const META_DATA_PATH = path.resolve(__dirname, '..', 'metadata.json'); + +export const ILLUSTRATION_NAME_REGEXP = /^([a-z0-9]|[a-z0-9](-?[a-z0-9])*-?[a-z0-9])$/; + +export const colorMaps = [ + {token: '--gil-color-object-base', lightRegexp: /#FFBE5C/gi}, + {token: '--gil-color-object-accent-heavy', lightRegexp: /#D36507/gi}, + {token: '--gil-color-object-hightlight', lightRegexp: /#FFD89D/gi}, + {token: '--gil-color-shadow-over-object', lightRegexp: /#D39E50/gi}, + {token: '--gil-color-background-lines', lightRegexp: /#8C8C8C/gi}, + {token: '--gil-color-background-shapes', lightRegexp: /#F2F2F2/gi}, + {token: '--gil-color-object-accent-light', lightRegexp: /#FFF(FFF)?/gi}, + {token: '--gil-color-object-danger', lightRegexp: /#FF003D/gi}, +]; + +export const themes = ['light', 'dark', 'light-hc', 'dark-hc']; diff --git a/scripts/generate-components-from-templates.mjs b/scripts/generate-components-from-templates.mjs new file mode 100644 index 0000000..c0a5d9f --- /dev/null +++ b/scripts/generate-components-from-templates.mjs @@ -0,0 +1,59 @@ +import fs from 'fs/promises'; +import path from 'path'; + +import {transform} from '@svgr/core'; + +import {ICONS_DIR, META_DATA_PATH, TEMPLATES_DIR} from './constants.mjs'; +import {cleanDir, prettify} from './utils.mjs'; + +async function createIndexFile(files) { + const indexFile = path.join(ICONS_DIR, 'index.ts'); + const content = files + .map((file) => { + const name = path.parse(file).name; + + return `export {default as ${name}} from './${name}'`; + }) + .join('\n'); + const prettyContent = await prettify(content, indexFile); + await fs.writeFile(indexFile, prettyContent); +} + +async function run() { + await cleanDir(ICONS_DIR); + + const metadata = JSON.parse(await fs.readFile(META_DATA_PATH, 'utf8')); + + const iconFiles = await Promise.all( + metadata.illustrations.map(async ({svgName, componentName}) => { + const filePath = path.resolve(TEMPLATES_DIR, `${svgName}.svg`); + + const code = await fs.readFile(filePath, 'utf8'); + try { + await fs.mkdir(path.join(ICONS_DIR)); + } catch (_e) {} + + const iconFile = path.join(ICONS_DIR, `${componentName}.tsx`); + const content = await transform( + code, + { + typescript: true, + plugins: ['@svgr/plugin-jsx'], + exportType: 'default', + }, + {componentName}, + ); + const prettyContent = await prettify(content, iconFile); + + await fs.writeFile(iconFile, prettyContent); + + return iconFile; + }), + ); + await createIndexFile(iconFiles); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/generate-svgs-from-templates.mjs b/scripts/generate-svgs-from-templates.mjs new file mode 100644 index 0000000..a5de450 --- /dev/null +++ b/scripts/generate-svgs-from-templates.mjs @@ -0,0 +1,40 @@ +import fsSync from 'fs'; +import fs from 'fs/promises'; +import path from 'path'; + +import {META_DATA_PATH, SVGS_DIR, TEMPLATES_DIR, themes} from './constants.mjs'; +import colorsConfig from './gravity-colors-config.mjs'; +import {svgoTransformer} from './transform-colors.mjs'; +import {cleanDir} from './utils.mjs'; + +async function writeToFile(filePath, fileName, data) { + if (!fsSync.existsSync(filePath)) { + await fs.mkdir(filePath, {recursive: true}); + } + await fs.writeFile(path.join(filePath, fileName), data); +} + +async function run() { + await cleanDir(SVGS_DIR); + + const metadata = JSON.parse(await fs.readFile(META_DATA_PATH, 'utf8')); + + metadata.illustrations.forEach(async ({svgName}) => { + const ill = await fs.readFile(path.resolve(TEMPLATES_DIR, `${svgName}.svg`), 'utf8'); + + themes.forEach(async (theme) => { + const transforms = Object.entries(colorsConfig[theme]).map(([token, color]) => ({ + newValue: color, + regexpOrStringToChange: `var(${token})`, + })); + const svg = await svgoTransformer(ill, transforms); + + await writeToFile(path.join(SVGS_DIR), `${svgName}-${theme}.svg`, svg); + }); + }); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/generate-templates.mjs b/scripts/generate-templates.mjs new file mode 100644 index 0000000..be18a7d --- /dev/null +++ b/scripts/generate-templates.mjs @@ -0,0 +1,37 @@ +import fsSync from 'fs'; +import fs from 'fs/promises'; +import path from 'path'; + +import {META_DATA_PATH, SVGS_DIR, TEMPLATES_DIR, colorMaps} from './constants.mjs'; +import {svgoTransformer} from './transform-colors.mjs'; +import {cleanDir} from './utils.mjs'; + +const transforms = colorMaps.map(({token, lightRegexp}) => ({ + newValue: `var(${token})`, + regexpOrStringToChange: lightRegexp, +})); + +async function writeToFile(filePath, fileName, data) { + if (!fsSync.existsSync(filePath)) { + await fs.mkdir(filePath, {recursive: true}); + } + await fs.writeFile(path.join(filePath, fileName), data); +} + +async function run() { + await cleanDir(TEMPLATES_DIR); + + const metadata = JSON.parse(await fs.readFile(META_DATA_PATH, 'utf8')); + + metadata.illustrations.forEach(async ({svgName}) => { + const ill = await fs.readFile(path.resolve(SVGS_DIR, `${svgName}-light.svg`), 'utf8'); + const svg = svgoTransformer(ill, transforms); + + await writeToFile(path.join(TEMPLATES_DIR), `${svgName}.svg`, svg); + }); +} + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/gravity-colors-config.mjs b/scripts/gravity-colors-config.mjs new file mode 100644 index 0000000..b653e95 --- /dev/null +++ b/scripts/gravity-colors-config.mjs @@ -0,0 +1,47 @@ +const colorsConfig = { + light: { + '--gil-color-object-base': 'rgb(255, 190, 92)', // --g-color-private-yellow-550-solid + '--gil-color-object-accent-heavy': 'rgb(211, 101, 7)', // --g-color-private-orange-650-solid + '--gil-color-object-hightlight': 'rgb(255, 216, 157)', // --g-color-private-yellow-350-solid + '--gil-color-shadow-over-object': 'rgb(211, 158, 80)', // --g-color-private-yellow-650-solid + '--gil-color-background-lines': 'rgb(140, 140, 140)', // --g-color-private-black-450-solid + '--gil-color-background-shapes': 'rgb(242, 242, 242)', // --g-color-private-black-50-solid + '--gil-color-object-accent-light': 'rgb(255, 255, 255)', // --g-color-private-white-1000-solid + '--gil-color-object-danger': 'rgb(255, 0, 61)', // --g-color-private-red-550-solid + }, + + dark: { + '--gil-color-object-base': 'rgb(255, 190, 92)', // --g-color-private-yellow-550-solid + '--gil-color-object-accent-heavy': 'rgb(211, 130, 61)', // --g-color-private-orange-650-solid + '--gil-color-object-hightlight': 'rgb(255, 210, 141)', // --g-color-private-yellow-700-solid + '--gil-color-shadow-over-object': 'rgb(233, 174, 86)', // --g-color-private-yellow-500-solid + '--gil-color-background-lines': 'rgb(156, 153, 156)', // --g-color-private-white-550-solid + '--gil-color-background-shapes': 'rgb(78, 74, 78)', // --g-color-private-white-200-solid + '--gil-color-object-accent-light': 'rgb(255, 255, 255)', // --g-color-private-white-1000-solid + '--gil-color-object-danger': 'rgb(229, 50, 93)', // --g-color-private-red-550-solid + }, + + 'light-hc': { + '--gil-color-object-base': 'rgb(255, 190, 92)', // --g-color-private-yellow-550-solid + '--gil-color-object-accent-heavy': 'rgb(208, 99, 4)', // --g-color-private-orange-650-solid + '--gil-color-object-hightlight': 'rgb(255, 216, 157)', // --g-color-private-yellow-350-solid + '--gil-color-shadow-over-object': 'rgb(208, 155, 77)', // --g-color-private-yellow-650-solid + '--gil-color-background-lines': 'rgb(140, 140, 140)', // --g-color-private-black-450-solid + '--gil-color-background-shapes': 'rgb(242, 242, 242)', // --g-color-private-black-50-solid + '--gil-color-object-accent-light': 'rgb(255, 255, 255)', // --g-color-private-white-1000-solid + '--gil-color-object-danger': 'rgb(255, 0, 61)', // --g-color-private-red-550-solid + }, + + 'dark-hc': { + '--gil-color-object-base': 'rgb(255, 190, 92)', // --g-color-private-yellow-550-solid + '--gil-color-object-accent-heavy': 'rgb(211, 130, 61)', // --g-color-private-orange-650-solid + '--gil-color-object-hightlight': 'rgb(255, 210, 141)', // --g-color-private-yellow-700-solid + '--gil-color-shadow-over-object': 'rgb(231, 173, 85)', // --g-color-private-yellow-500-solid + '--gil-color-background-lines': 'rgb(148, 148, 148)', // --g-color-private-white-550-solid + '--gil-color-background-shapes': 'rgb(65, 65, 65)', // --g-color-private-white-200-solid + '--gil-color-object-accent-light': 'rgb(255, 255, 255)', // --g-color-private-white-1000-solid + '--gil-color-object-danger': 'rgb(229, 50, 93)', // --g-color-private-red-550-solid + }, +}; + +export default colorsConfig; diff --git a/scripts/transform-colors.mjs b/scripts/transform-colors.mjs new file mode 100644 index 0000000..9a8654f --- /dev/null +++ b/scripts/transform-colors.mjs @@ -0,0 +1,62 @@ +import {optimize} from 'svgo'; +import {colorsProps} from 'svgo/plugins/_collections.js'; + +function getColorsTransformatorPlagin(transforms) { + return { + name: 'change-colors-to-css-vars', + fn: () => { + return { + element: { + enter: (node) => { + for (const [name, value] of Object.entries(node.attributes)) { + if (colorsProps.has(name)) { + for (const {newValue, regexpOrStringToChange} of transforms) { + const matched = + typeof regexpOrStringToChange === 'string' + ? regexpOrStringToChange === value + : regexpOrStringToChange.test(value); + + if (matched) { + node.attributes[name] = newValue; + } + } + } + } + return null; + }, + }, + }; + }, + }; +} + +/** + * The complete Triforce, or one or more components of the Triforce. + * @typedef {Object} Transforms + * @property {string} newValue + * @property {(RegExp | string)} regexpOrStringToChange + */ + +/** + * @param {string} svgString + * @param {Transforms[]} [transforms] + * + * @return {string} + */ +export function svgoTransformer(svgString, transforms) { + return optimize(svgString, { + multipass: true, + plugins: [ + {name: 'removeAttrs', params: {attrs: ['id']}}, + transforms ? getColorsTransformatorPlagin(transforms) : undefined, + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ].filter(Boolean), + }).data; +} diff --git a/scripts/utils.mjs b/scripts/utils.mjs new file mode 100644 index 0000000..c99c486 --- /dev/null +++ b/scripts/utils.mjs @@ -0,0 +1,13 @@ +import fs from 'fs/promises'; + +import prettier from 'prettier'; + +export async function cleanDir(dir) { + await fs.rm(dir, {recursive: true, force: true}); + await fs.mkdir(dir, {recursive: true}); +} + +export async function prettify(content, filepath) { + const prettierConfig = (await prettier.resolveConfig(filepath)) ?? {}; + return prettier.format(content, {...prettierConfig, filepath}); +}