Skip to content

Commit

Permalink
Create a packages to distribute the icons for React 16 projects (#124)
Browse files Browse the repository at this point in the history
Co-authored-by: vicky-comeau <[email protected]>
  • Loading branch information
alexasselin008 and vicky-comeau authored Jan 9, 2024
1 parent dff7da1 commit f685e70
Show file tree
Hide file tree
Showing 168 changed files with 2,455 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"changelog": ["@changesets/cli/changelog", { "repo": "gsoft-inc/wl-hopper" }],
"commit": false,
"fixed": [],
"linked": [],
"linked": [["@hopper-ui/icons-react16", "@hopper-ui/icons"]],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"build:pkg": "pnpm -r --filter \"{packages/**}\" build ",
"changeset": "changeset",
"ci-release": "pnpm build && changeset publish",
"generate-icons": "pnpm --filter=\"svg-icons\" generate-icons && pnpm --filter=\"@hopper-ui/icons\" generate-icons",
"generate-icons": "pnpm --filter=\"svg-icons\" generate-icons && pnpm --filter=\"@hopper-ui/icons*\" generate-icons",
"lint": "pnpm run \"/^lint:.*/\" && pnpm run typecheck",
"lint:eslint": "eslint . --max-warnings=-1 --cache --cache-location node_modules/.cache/eslint",
"lint:style": "stylelint \"**/*.css\" --allow-empty-input --cache --cache-location node_modules/.cache/stylelint",
Expand Down
5 changes: 5 additions & 0 deletions packages/icons-react16/.eslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/eslintrc",
"root": true,
"extends": "plugin:@workleap/react-library"
}
64 changes: 64 additions & 0 deletions packages/icons-react16/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# @hopper-ui/icons-react16

A set of icons handcrafted by Workleap. This package is meant to be temporary, to allow teams that are still using React 16 to be able to have access to the shared icons.

> This package assumes that you are importing the CSS tokens from Hopper in your application. If you are not, icon colors will not be applied.
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](../../LICENSE)
[![npm version](https://img.shields.io/npm/v/@hopper-ui/icons-react16)](https://www.npmjs.com/package/@hopper-ui/icons-react16)

## Installation

### Install packages

**With pnpm**

```shell
pnpm add @hopper-ui/icons-react16
```

**With yarn**

```shell
yarn add -D @hopper-ui/icons-react16
```

**With npm**

```shell
npm install -D @hopper-ui/icons-react16
```

### Import Styles
```css
/* in your root css */
@import "@hopper-ui/icons-react16/index.css";
```


https://wl-hopper.netlify.app/icons/react-icons/standalone-installation#import-styles

### Start using icons

```tsx
import { AddIcon } from "@hopper-ui/icons-react16";

export const App = () => (
<div>
<span>Hello World!</span>
<AddIcon size="sm" />
</div>
);
```

## Available Icons

View the [library](https://wl-hopper.netlify.app/icons/react-icons/library).

## 🤝 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.
63 changes: 63 additions & 0 deletions packages/icons-react16/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@hopper-ui/icons-react16",
"author": "Workleap",
"version": "1.0.2",
"description": "The icons package that targets React 16.",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/gsoft-inc/wl-hopper.git",
"directory": "packages/icons-react16"
},
"publishConfig": {
"access": "public",
"provenance": true
},
"type": "module",
"sideEffects": false,
"files": [
"/dist",
"CHANGELOG.md",
"README.md"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"style": "dist/index.css",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./index.css": "./dist/index.css"
},
"scripts": {
"build": "tsup --config ./tsup.build.ts",
"generate-icons": "tsx scripts/build.ts"
},
"peerDependencies": {
"react": "^16",
"react-dom": "^16"
},
"devDependencies": {
"react": "^16",
"react-dom": "^16",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/plugin-svgo": "^8.1.0",
"@swc/core": "1.3.96",
"@swc/helpers": "0.5.3",
"@types/node": "^20.9.3",
"@types/react": "^16",
"@types/react-dom": "^16",
"@workleap/eslint-plugin": "3.0.0",
"@workleap/swc-configs": "2.1.2",
"@workleap/typescript-configs": "3.0.2",
"identity-obj-proxy": "3.0.0",
"ts-jest": "29.1.1",
"ts-node": "10.9.1",
"tsup": "8.0.0",
"tsx": "4.1.4",
"typescript": "5.3.2"
}
}
17 changes: 17 additions & 0 deletions packages/icons-react16/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Purpose: Build script for the icons package.

import { ComponentDirectory, SVGsDirectory } from "./constants.ts";
import { fetchSvgs } from "./fetch-svgs.ts";
import { generateComponents } from "./generate-components.ts";
import { generateIndex } from "./generate-index.ts";

console.log("⚙️ Fetching SVGs...\n");
const multiSourceIcons = fetchSvgs(SVGsDirectory);

console.log("⚙️ Generating react components...\n");
generateComponents(ComponentDirectory, multiSourceIcons);

console.log("📋 List of icons generation...\n");
generateIndex(ComponentDirectory, multiSourceIcons);

console.log("✨ Build completed!\n");
6 changes: 6 additions & 0 deletions packages/icons-react16/scripts/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const ComponentDirectory = "src/generated-icon-components";
export const SVGsDirectory = "../svg-icons/src/optimized-icons/";
export const IconSizes = [16, 24, 32] as const;

export const NeutralIconColor = "#3C3C3C"; // --hop-neutral-icon
export const PrimaryIconColor = "#3B57FF"; // --hop-primary-icon
41 changes: 41 additions & 0 deletions packages/icons-react16/scripts/fetch-svgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import fs from "fs";
import path from "path";
import type { IconSizes } from "./constants.ts";

export interface MultiSourceIconSource {
name: string;
sizes: Record<typeof IconSizes[number], string>;
}

const fromKebabToPascalCase = (str: string) => {
return str.split("-").map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("");
};

export const fetchSvgs = (SVGsDir: string) => {
const exists = fs.existsSync(SVGsDir);
if (!exists) {
throw new Error(`Directory, ${SVGsDir}, does not exist.`);
}

const files = fs.readdirSync(SVGsDir, { recursive: true, withFileTypes: true });

const svgFilePaths = files.filter(file => file.isFile() && path.extname(file.name) === ".svg").map(file => {
return path.resolve(file.path, file.name);
});

const dict: Record<string, MultiSourceIconSource> = {};

svgFilePaths.forEach(svgFilePath => {
const svg = fs.readFileSync(svgFilePath, "utf8");
const name = path.basename(svgFilePath, ".svg");
const baseName = name.replace(/-\d+$/, "");
const size = Number(name.split("-").pop());

dict[baseName] = {
name: fromKebabToPascalCase(baseName),
sizes: { ...dict[baseName]?.sizes, [size]: svg }
};
});

return Object.values(dict);
};
53 changes: 53 additions & 0 deletions packages/icons-react16/scripts/generate-components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { transform } from "@svgr/core";
import fs from "fs";
import path from "path";
import { PrimaryIconColor } from "./constants.ts";
import type { MultiSourceIconSource } from "./fetch-svgs.ts";
import svgoConfig from "./svgo-config.ts";

export async function generateComponents(componentDirectory: string, icons: MultiSourceIconSource[]) {
// Clear directory (It also removes the directory itself)
fs.rmSync(componentDirectory, { recursive: true, force: true });
fs.mkdirSync(componentDirectory, { recursive: true });

for (const icon of icons) {
let componentCode = [
"/**",
" * This file is generated by the generate-components script. Do not edit directly.",
" */",
"/* eslint-disable */",
"import { createIcon } from \"../create-icon.tsx\";",
"import React, { forwardRef, type Ref, type SVGProps } from \"react\";"
].join("\n");
componentCode += "\n\n";

const baseIconName = `${icon.name}Icon`;

for (const [size, data] of Object.entries(icon.sizes)) {
componentCode += transform.sync(data, {
typescript: true,
ref: true,
replaceAttrValues: {
[PrimaryIconColor]: "var(--hop-primary-icon)"
},
jsxRuntime: "automatic",
svgoConfig: svgoConfig,
plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"],
template: ({ componentName, jsx, props }, { tpl }) => {
return tpl`
const ${componentName} = forwardRef((${props}) => (
${jsx}
));
`;
}
}, {
componentName: `${baseIconName}${size}`
});
componentCode += "\n";
}
componentCode += `\nexport const ${baseIconName} = createIcon(${baseIconName}16, ${baseIconName}24, ${baseIconName}32, "${baseIconName}");`;

const destinationPath = path.resolve(componentDirectory, baseIconName + ".tsx");
fs.writeFileSync(destinationPath, Buffer.from(componentCode));
}
}
17 changes: 17 additions & 0 deletions packages/icons-react16/scripts/generate-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import fs from "fs";
import type { MultiSourceIconSource } from "./fetch-svgs.ts";

const GENERATED_HEADER = `/*
* This file is generated by the generate-components script. Do not edit directly.
*/\n
/* eslint-disable */`;

export const generateIndex = (componentDirectory: string, iconsByNames: MultiSourceIconSource[]) => {
const iconList = iconsByNames.map(icon => icon.name + "Icon");
const indexFile = `${componentDirectory}/index.ts`;
const indexContent = `${GENERATED_HEADER}\n
${Object.values(iconsByNames).map(icon => `export * from "./${icon.name}Icon.tsx";`).join("\n")}
\nexport const iconNames = ${JSON.stringify(iconList)} as const;`;

fs.writeFileSync(indexFile, indexContent);
};
17 changes: 17 additions & 0 deletions packages/icons-react16/scripts/svgo-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Config } from "svgo";

const config : Config = {
plugins: [
{
name: "preset-default",
params: {
overrides: {
removeViewBox: false
}
}
},
"removeXMLNS"
]
};

export default config;
5 changes: 5 additions & 0 deletions packages/icons-react16/src/Icon.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.hop-icon {
display: inline-block;
pointer-events: none;
flex-shrink: 0;
}
62 changes: 62 additions & 0 deletions packages/icons-react16/src/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { forwardRef, type ElementType, type RefAttributes, type SVGProps, type ComponentProps } from "react";
import styles from "./Icon.module.css";

export interface IconProps extends Omit<ComponentProps<"svg">, "ref"> {
/**
* The size of the icon.
*/
size?: "sm" | "md" | "lg";
/**
* The source of the icon with a size of 16px.
*/
src16: ElementType<Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>>;
/**
* The source of the icon with a size of 24px.
*/
src24: ElementType<Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>>;
/**
* The source of the icon with a size of 32px.
*/
src32: ElementType<Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>>;
}

export const Icon = forwardRef<SVGSVGElement, IconProps>((props, ref) => {
const {
size = "md",
src16,
src24,
src32,
style,
className,
"aria-label": ariaLabel,
"aria-hidden": ariaHidden,
...rest
} = props;

const sizeMappings = {
sm: src16,
md: src24,
lg: src32
};

const As = sizeMappings[size];
const classNames = [
styles["hop-icon"],
className
].filter(x => x !== undefined).join(" ");

return (
<As
style={style}
{...rest}
ref={ref}
focusable="false"
role="img"
aria-label={ariaLabel}
aria-hidden={(ariaLabel ? (ariaHidden || undefined) : true)}
className={classNames}
/>
);
});

Icon.displayName = "Icon";
Loading

0 comments on commit f685e70

Please sign in to comment.