diff --git a/.changeset/few-actors-attack.md b/.changeset/few-actors-attack.md new file mode 100644 index 000000000..e3e7fd2d1 --- /dev/null +++ b/.changeset/few-actors-attack.md @@ -0,0 +1,5 @@ +--- +"@hopper-ui/svg-icons": major +--- + +Initial release of the package diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d64f36fa4..29814c500 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: - name: Install Node.js uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" - uses: pnpm/action-setup@v2 name: Install pnpm @@ -38,7 +40,7 @@ jobs: shell: bash run: | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - + - name: Setup pnpm cache uses: actions/cache@v3 with: @@ -46,7 +48,7 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - + - name: Install dependencies run: pnpm i --frozen-lockfile diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..b009dfb9d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/* diff --git a/package.json b/package.json index 325d01191..cc4b5acce 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "doc:start": "pnpm --filter=docs dev", "doc:storybook": "pnpm --filter=docs storybook", "doc:build": "pnpm --filter=docs build", - "test": "jest", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "build": "pnpm -r build ", "build:tokens": "pnpm --filter=\"@hopper-ui/tokens\" build", "build:pkg": "pnpm -r --filter \"{packages/**}\" build ", @@ -36,7 +36,7 @@ "devDependencies": { "@changesets/cli": "2.26.2", "@hopper-ui/tokens": "workspace:*", - "@netlify/plugin-nextjs": "4.41.1", + "@netlify/plugin-nextjs": "4.41.2", "@storybook/addon-essentials": "7.5.3", "@storybook/addon-interactions": "7.5.3", "@storybook/addon-links": "7.5.3", @@ -45,14 +45,15 @@ "@storybook/react-vite": "7.5.3", "@storybook/react": "7.5.3", "@storybook/testing-library": "0.2.2", - "@types/jest": "29.5.8", - "@types/node": "20.9.0", + "@types/jest": "29.5.9", + "@types/node": "20.9.3", "@workleap/eslint-plugin": "3.0.0", "@workleap/stylelint-configs": "2.0.0", "@workleap/typescript-configs": "3.0.2", + "cross-env": "7.0.3", "eslint-plugin-storybook": "0.6.15", "jest": "29.7.0", - "prettier": "2.8.8", + "prettier": "3.1.0", "prop-types": "15.8.1", "react-dom": "18.2.0", "react": "18.2.0", diff --git a/packages/components/package.json b/packages/components/package.json index 054fc7e8f..0fa9afd8b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -56,7 +56,6 @@ "react-test-renderer": "18.2.0", "react": "18.2.0", "ts-jest": "29.1.1", - "ts-node": "10.9.1", "tsup": "7.2.0", "typescript": "5.2.2" }, diff --git a/packages/styled-system/package.json b/packages/styled-system/package.json index 44dcadbb6..e8229da56 100644 --- a/packages/styled-system/package.json +++ b/packages/styled-system/package.json @@ -67,7 +67,6 @@ "react-test-renderer": "18.2.0", "react": "18.2.0", "ts-jest": "29.1.1", - "ts-node": "10.9.1", "tsup": "7.2.0", "type-fest": "4.7.1", "typescript": "5.2.2" diff --git a/packages/svg-icons/.eslintrc.json b/packages/svg-icons/.eslintrc.json new file mode 100644 index 000000000..d9f48339d --- /dev/null +++ b/packages/svg-icons/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc", + "root": true, + "extends": "plugin:@workleap/typescript-library" +} diff --git a/packages/svg-icons/README.md b/packages/svg-icons/README.md new file mode 100644 index 000000000..6f52a9fa5 --- /dev/null +++ b/packages/svg-icons/README.md @@ -0,0 +1,40 @@ +# @hopper-ui/svg-icons + +A set of icons handcrafted by Workleap. + +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](../../LICENSE) +[![npm version](https://img.shields.io/npm/v/@hopper-ui/svg-icons)](https://www.npmjs.com/package/@hopper-ui/svg-icons) + +## Installation + +Install the following packages: + +**With pnpm** + +```shell +pnpm add @hopper-ui/svg-icons +``` + +**With yarn** + +```shell +yarn add -D @hopper-ui/svg-icons +``` + +**With npm** + +```shell +npm install -D @hopper-ui/svg-icons +``` + +## Usage + +View the [user's documentation](https://hopper.workleap.design/). + +## 🤝 Contributing + +View the [contributor's documentation](https://github.com/gsoft-inc/wl-hopper/blob/main/CONTRIBUTING.md). + +## License + +Copyright © 2023, Workleap. This code is licensed under the Apache License, Version 2.0. You may obtain a copy of this license at https://github.com/gsoft-inc/workleap-license/blob/master/LICENSE. diff --git a/packages/svg-icons/jest.config.ts b/packages/svg-icons/jest.config.ts new file mode 100644 index 000000000..83ce3969d --- /dev/null +++ b/packages/svg-icons/jest.config.ts @@ -0,0 +1,12 @@ +import type { Config } from "jest"; +import { swcConfig } from "./swc.jest.ts"; + +const config: Config = { + testEnvironment: "node", + transform: { + "^.+\\.(js|ts|tsx)$": ["@swc/jest", swcConfig as Record] + }, + extensionsToTreatAsEsm: [".ts"] +}; + +export default config; diff --git a/packages/svg-icons/package.json b/packages/svg-icons/package.json new file mode 100644 index 000000000..a55a14560 --- /dev/null +++ b/packages/svg-icons/package.json @@ -0,0 +1,50 @@ +{ + "name": "@hopper-ui/svg-icons", + "author": "Workleap", + "version": "0.0.0", + "description": "A set of icons handcrafted by Workleap", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/gsoft-inc/wl-hopper.git", + "directory": "packages/svg-icons" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "type": "module", + "sideEffects": "*.svg", + "files": [ + "/dist", + "CHANGELOG.md", + "README.md" + ], + "exports": { + "./icons/*.svg": "./dist/icons/*.svg" + }, + "scripts": { + "build": "tsx scripts/build.ts" + }, + "devDependencies": { + "@swc/core": "1.3.96", + "@swc/helpers": "0.5.3", + "@swc/jest": "0.2.29", + "@types/jest": "29.5.9", + "@types/node": "^20.9.3", + "@workleap/eslint-plugin": "3.0.0", + "@workleap/swc-configs": "2.1.2", + "@workleap/typescript-configs": "3.0.2", + "hast-util-select": "6.0.2", + "jest": "29.7.0", + "rehype-parse": "9.0.0", + "svgo": "^3.0.4", + "tsx": "4.1.4", + "typescript": "5.3.2", + "unified": "11.0.4" + + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/svg-icons/scripts/build.ts b/packages/svg-icons/scripts/build.ts new file mode 100644 index 000000000..69b68c910 --- /dev/null +++ b/packages/svg-icons/scripts/build.ts @@ -0,0 +1,31 @@ +import path from "path"; +import { IconSizes, IconsDistDirectory, IconsSourceDirectory } from "./constants.ts"; +import { generateIcons } from "./generate-icons.ts"; + +/** + * Converts a file path to a file name. + * @example fileNameConverter("C:\\Dev\\wl-hopper\\packages\\svgs\\src\\icons\\16px\\Add.svg") // add-16.svg + */ +function fileNameConverter(filePath: string) { + const dirName = path.dirname(filePath); + const size = path.basename(dirName).replace("px", ""); + + if ((IconSizes as readonly number[]).includes(Number(size)) === false) { + throw new Error(`Invalid icon size: ${size}`); + } + + const fileName = path.basename(filePath, ".svg"); + + const kebabCaseName = fileName + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replace(/[\s_]+/g, "-") + .toLowerCase(); + + return `${kebabCaseName}-${size}.svg`; +} + +console.log("⚙️ Optimizing icons...\n"); + +generateIcons(IconsSourceDirectory, IconsDistDirectory, fileNameConverter); + +console.log("✨ The icons have been optimized!\n"); diff --git a/packages/svg-icons/scripts/constants.ts b/packages/svg-icons/scripts/constants.ts new file mode 100644 index 000000000..c53f96909 --- /dev/null +++ b/packages/svg-icons/scripts/constants.ts @@ -0,0 +1,5 @@ +export const IconsSourceDirectory = "src/icons"; +export const IconsDistDirectory = "dist/icons"; +export const IconSizes = [16, 24, 32] as const; +export const NeutralIconColor = "#3C3C3C"; // --hop-neutral-icon +export const PrimaryIconColor = "#3B57FF"; // --hop-primary-icon diff --git a/packages/svg-icons/scripts/generate-icons.ts b/packages/svg-icons/scripts/generate-icons.ts new file mode 100644 index 000000000..6c7f3d383 --- /dev/null +++ b/packages/svg-icons/scripts/generate-icons.ts @@ -0,0 +1,62 @@ +import fs from "fs"; +import path from "path"; +import { optimize } from "svgo"; +import config from "./svgo-config.ts"; + +function ensureDirSync(dir: string) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +} + +export function generateIcons(srcDir: string, outputDir: string, fileNameConverter?: (filePath: string) => string) { + ensureDirSync(outputDir); + + // This line requires Node.js 20.5.2 or higher to execute properly + // https://github.com/nodejs/node/issues/48858 + const files = fs.readdirSync(srcDir, { recursive: true, withFileTypes: true }); + + const svgFiles = files.filter(file => file.isFile() && file.name.endsWith(".svg")); + + const iconFiles = svgFiles.map(file => { + const srcPath = path.resolve(file.path, file.name); + const dstPath = path.resolve(outputDir, fileNameConverter ? fileNameConverter(srcPath) : file.name); + + return { + srcPath: srcPath, + dstPath + }; + }); + + // If it's possible to rename a file with the same name, then we need to validate that there are no duplicates + if (fileNameConverter) { + validateNoNameDuplicate(iconFiles.map(x => x.dstPath)); + } + + iconFiles.forEach(iconFile => { + const contents = fs.readFileSync(iconFile.srcPath, "utf8"); + const { data } = optimize(contents, { + path: iconFile.srcPath, + ...config + }); + + fs.writeFileSync(iconFile.dstPath, Buffer.from(data)); + }); +} + +function validateNoNameDuplicate(names: string[]) { + const duplicateNames: string[] = []; + const nameSet = new Set(); + + for (const name of names) { + if (nameSet.has(name)) { + duplicateNames.push(name); + } else { + nameSet.add(name); + } + } + + if (duplicateNames.length > 0) { + throw new Error(`Duplicate icon names detected: ${duplicateNames.join(", ")}`); + } +} diff --git a/packages/svg-icons/scripts/svgo-config.ts b/packages/svg-icons/scripts/svgo-config.ts new file mode 100644 index 000000000..fc61e33dc --- /dev/null +++ b/packages/svg-icons/scripts/svgo-config.ts @@ -0,0 +1,61 @@ +import type { Config } from "svgo"; +import { NeutralIconColor } from "./constants.ts"; + +const config: Config = { + multipass: true, + plugins: [ + { + name: "preset-default", + params: { + overrides: { + /** + * viewBox is needed in order to produce 20px by 20px containers + * with smaller icons inside. + */ + removeViewBox: false, + /** + * Some of our icons have multiple fill colors. We want to keep them, but replace the main icon color + * with the currentColor value. This allows us to change the color of the icon with the color CSS property. + */ + convertColors: { + currentColor: NeutralIconColor + } + + } + } + }, + /** + * Converts presentation attributes in element styles to the equivalent XML attribute. + * Presentation attributes can be used in both attributes and styles, but in most cases it'll take fewer bytes to use attributes + */ + { name: "convertStyleToAttrs" }, + /** + * Remove all