Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 14 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended'
],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'prettier/prettier': 'error'
}
};
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ build/Release
node_modules/
jspm_packages/

package-lock.json
dist/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

Expand Down
15 changes: 15 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
src/
.github/
.husky/
.eslintrc.js
.prettierrc
tsconfig.json
build-icons.ts
*.config.js
.gitignore
.git/
.env
.env.*
.vscode/
*.tsx
*.map
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "all",
"arrowParens": "always"
}
11 changes: 11 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/git",
"@semantic-release/github"
]
}
87 changes: 85 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,85 @@
# icons
The set of icons from CtrlCloud for DevOps projects
## 🌐 Icons
Each service is a self-contained React component and can be resized using standard `width` and `height` props.
We use optimized SVGs with optional `colors[]` and `colorsByHex{}` support, so they adapt their colors recursively and behave consistently across your app.


### 🛠️ Setup


#### install
```bash
npm i @ctrlcloud/icons
```
#### basic usage
```js
import {EC2} from '@ctrlcloud/icons';

export default function Home() {
return (
<div className="flex gap-4">
<EC2 />
</div>
);
}
````
#### Next.js Config
To use this library in a Next.js app, add the following to your `next.config.js`:

```js

transpilePackages: ['@ctrlcloud/icons'], // Explicitly transpile this package
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
});
return config;
},
```

Also install the following package:

```bash
npm install --save-dev @svgr/webpack
```

### Attributes
All props are optional:


| Name | Type | Description |
|:---------|:-----|:------------|
| width | number | Sets the width of the icon. Defaults to `40px`. |
| height | number | Sets the height of the icon. Defaults to `40px`. |
| colors | string[] | Replaces colors in order of appearance (first color replaces first fill, etc.)|
| colorsByHex | object | Precise color overrides: { "ORIGINAL_HEX": "NEW_HEX" }. |
| ...props | any | All other SVG-compatible props like `className`, `style`, `aria-label`, etc. |
---
💡 Tip: Use your browser's inspector to check the original SVG colors before replacing

### Usage


Example usage:

```jsx
import { EC2, S3, Lambda } from '@ctrlcloud/icons';

export default function Example() {
return (
<div style={{ display: 'flex', gap: '16px' }}>
<S3 width={48} height={48} />

// Replace first color in svg:
<Lambda width={48} height={48} colors={['#FF9900']} />

// You can also override specific colors easily:
<EC2 colorsByHex={{ "#ED7100": "#00ADEF", "#FFFFFF": "#F7FAFC" }} />
</div>
);
}

```
---
Enjoy using `icons` in your project! 🌐🌟

154 changes: 154 additions & 0 deletions build-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import fs from 'fs/promises';
import path from 'path';
import { transform } from '@svgr/core';

const ICONS_DIR = path.join(process.cwd(), 'src/icons');
const OUT_DIR = path.join(process.cwd(), 'dist/icons');
const OUT_DIR_INDEX = path.join(process.cwd(), 'dist');

type IIconExport = {
componentName: string;
importPath: string;
}

async function processDirectory(dir: string, relativePath = ''): Promise<IIconExport[]> {
const files = await fs.readdir(dir);
let allExports: IIconExport[] = [];

for (const file of files) {
const fullPath = path.join(dir, file);
const relativeFilePath = path.join(relativePath, file);
const stat = await fs.stat(fullPath);

if (stat.isDirectory()) {
const subDirExports = await processDirectory(fullPath, relativeFilePath);
allExports = [...allExports, ...subDirExports];
} else if (file.endsWith('.svg')) {
const svgCode = await fs.readFile(fullPath, 'utf8');
const componentName = path.basename(file, '.svg');
const pascalCaseName = componentName.replace(/(^|-)(\w)/g, (_, __, char) => char.toUpperCase());

const jsCode = await transform(
svgCode,
{
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx', '@svgr/plugin-prettier'],
typescript: false,
jsxRuntime: 'classic',
expandProps: 'end',
template: ({ componentName, jsx }, { tpl }) => tpl`
import React from 'react';

const ${componentName} = ({
width = 40,
height = 40,
colors = [],
colorsByHex = {},
...props
}) => {
const originalChildren = ${jsx}.props.children;

const processChildren = (children) => {
return React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return child;

let newChild = child;

if (child.props.fill) {
const overrideFill = colorsByHex[child.props.fill] || colors[index] || child.props.fill;
newChild = React.cloneElement(child, { fill: overrideFill });
}

if (child.props.children) {
const processedChildren = processChildren(child.props.children);
newChild = React.cloneElement(child, {}, processedChildren);
}

return newChild;
});
};

const processedChildren = processChildren(originalChildren);

return (
<svg
{...${jsx}.props}
width={width}
height={height}
{...props}
>
{processedChildren}
</svg>
);
};

export default ${componentName};
`
},
{ componentName: pascalCaseName }
);


const outSubDir = path.join(OUT_DIR, relativePath);
await fs.mkdir(outSubDir, { recursive: true });

await fs.writeFile(
path.join(outSubDir, `${pascalCaseName}.jsx`),
jsCode
);

const dtsContent =
`import * as React from 'react';
import type { SVGProps } from 'react';

type ICustomIconProps = SVGProps<SVGSVGElement> & {
colors?: string[];
colorsByHex?: Record<string, string>;
width?: number | string;
height?: number | string;
}

declare const ${pascalCaseName}: React.FC<ICustomIconProps>;
export default ${pascalCaseName};`;
await fs.writeFile(path.join(outSubDir, `${pascalCaseName}.d.ts`), dtsContent);

const exportPath = path.join('icons', relativePath, pascalCaseName);
allExports.push({
componentName: pascalCaseName,
importPath: exportPath.replace(/\\/g, '/'),
});
}
}

return allExports;
}

async function buildIcons() {
try {
await fs.mkdir(OUT_DIR, { recursive: true });

const allExports = await processDirectory(ICONS_DIR);

const indexContent = allExports
.map(({ componentName, importPath }) =>
`export { default as ${componentName} } from './${importPath}.jsx';`
)
.join('\n');

await fs.writeFile(path.join(OUT_DIR_INDEX, 'index.js'), indexContent);

const dtsContent = allExports
.map(({ componentName, importPath }) =>
`export { default as ${componentName} } from './${importPath}.js';`
)
.join('\n');

await fs.writeFile(path.join(OUT_DIR_INDEX, 'index.d.ts'), dtsContent);

console.log(`Generated ${allExports.length} icon components`);
} catch (error) {
console.error('Error building icons:', error);
process.exit(1);
}
}

buildIcons();
Loading