Lucide 0.15.0 (#272)

* add configs

* Add vue components

* Add documentation

* add alpha release version

* improve npm ignore files

* add tests

* Make style and class attrs work

* 📦 bump version

* Add Icon suffix for component names

* bump version

* Add icon component example

* remove space

* add new build strategy

* Write a better intro

* add other node design

* fix

* add new default template

* add tempalte

* improve code

* small improvements

* small improvements

* move files

* Connect lucide with lucide-react

* Add support for vue

* Add licenses to packages

* Fix tests

* refactor build scripts

* Minor code fixes

* update homepage readme

* Update footer text

* Add a better introduction to packages

* Split up in tempaltes

* Add new types build file

* Setup workflow file

* update readme

* update

* Fix build

* remove debug code

* Add check if svgs have duplicated children

* Add check if their are no children

* small fixes

* last fixes in the build

* Move script to packages folder

* Fix tests and add types for lucide

* Add rule to package.json

* add types in build

* add npm ignore

* update package.jsons
This commit is contained in:
Eric Fennis
2021-03-23 19:26:50 +01:00
committed by GitHub
parent 9d101a5275
commit b4e4f002f2
81 changed files with 5804 additions and 1655 deletions

View File

@@ -2,6 +2,7 @@
"name": "lucide-figma",
"version": "0.13.0",
"license": "ISC",
"private": true,
"main": "build/ui.js",
"scripts": {
"build": "webpack --mode=production",

View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2020, Lucide Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,6 +1,8 @@
# Lucide React
Use the lucide icon library in you react app.
Implementation of the lucide icon library for react applications.
> What is lucide? Read it [here](https://github.com/lucide-icons/lucide#what-is-lucide).
## Installation

View File

@@ -1,3 +1,6 @@
const mainConfig = require('../../babel.config');
module.exports = {
presets: ['react-app'],
env: mainConfig.env,
};

View File

@@ -1,54 +0,0 @@
const fs = require("fs")
const path = require("path")
const srcDirectory = path.join(__dirname, "dist")
// Declare type definitions
const typeDefinitions = `
/// <reference types="react" />
import { SVGAttributes } from 'react'
// Create interface extending SVGAttributes
export interface LucideProps extends Partial<React.SVGProps<SVGSVGElement>> {
color?: string
size?: string | number
stroke?: string | number
strokeWidth?: string | number
}
// Generated icons
`
// Write type definitions to file
fs.writeFileSync(
path.join(srcDirectory, "index.d.ts"),
typeDefinitions,
"utf-8",
)
const iconsFolder = "./build/icons"
fs.readdir(iconsFolder, (err, files) => {
files.forEach(file => {
const fileName = file.replace(".js", "")
// Convert fileName to component name
const componentName = fileName.split("-")
.map(word => word[0].toUpperCase() + word.substr(1, word.length))
.join("")
// Declare component type
const exportTypeString = `export declare const ${componentName}: (props: LucideProps) => JSX.Element;\n`
// Add component to the types file
fs.appendFileSync(
path.join(srcDirectory, "index.d.ts"),
exportTypeString,
"utf-8",
)
})
console.log("Generated index.d.ts file with", files.length, "icons")
})

View File

@@ -3,9 +3,6 @@ module.exports = {
roots: ['<rootDir>/src/', '<rootDir>/tests/'],
moduleFileExtensions: ['js'],
transformIgnorePatterns: [`/node_modules`],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.js$': 'babel-jest',
},

View File

@@ -16,14 +16,13 @@
"main:umd": "dist/umd/lucide-react.js",
"module": "dist/esm/lucide-react.js",
"unpkg": "dist/umd/lucide-react.min.js",
"typings": "dist/index.d.ts",
"typings": "dist/lucide-react.d.ts",
"scripts": {
"build": "yarn clean && yarn build:move && yarn build:icons && yarn build:es && yarn build:types && yarn build:bundles",
"clean": "rm -rf dist && rm -rf build",
"build:move": "cp -av src build",
"build:icons": "yarn --cwd ../../ build:icons --output=../packages/lucide-react/build --templateSrc=../packages/lucide-react/scripts/exportTemplate --camelizeAttrs --noDefaultAttrs --renderUniqueKey",
"build:types": "node build-types.js",
"build:es": "yarn --cwd ../../ babel packages/lucide-react/build -d packages/lucide-react/dist/esm",
"build": "yarn clean && yarn build:icons && yarn build:es && yarn build:types && yarn build:bundles",
"clean": "rm -rf dist && rm -rf ./src/icons/*.js",
"build:icons": "yarn --cwd ../../ build:icons --output=../packages/lucide-react/src --templateSrc=../packages/lucide-react/scripts/exportTemplate --renderUniqueKey",
"build:es": "yarn --cwd ../../ babel packages/lucide-react/src -d packages/lucide-react/dist/esm",
"build:types": "yarn --cwd ../../ babel-node packages/lucide-react/scripts/buildTypes.js",
"build:bundles": "yarn --cwd ../../ rollup -c packages/lucide-react/rollup.config.js",
"test": "jest"
},

View File

@@ -1,11 +1,11 @@
const plugins = require('../../rollup.plugins');
const pkg = require('./package.json');
import plugins from '../../rollup.plugins';
import pkg from './package.json';
const packageName = 'LucideReact';
const outputFileName = 'lucide-react';
const rootDir = 'packages/lucide-react'; // It runs from the root
const outputDir = `${rootDir}/dist`;
const inputs = [`${rootDir}/build/lucide-react.js`];
const inputs = [`${rootDir}/src/lucide-react.js`];
const bundles = [
{
format: 'umd',
@@ -30,7 +30,7 @@ const configs = bundles
inputs.map(input => ({
input,
plugins: plugins(pkg, minify),
external: ['react', 'prop-types'],
external: ['react', 'prop-types', 'lucide'],
output: {
name: packageName,
file: `${outputDir}/${format}/${outputFileName}${minify ? '.min' : ''}.js`,
@@ -39,6 +39,7 @@ const configs = bundles
globals: {
react: 'react',
'prop-types': 'PropTypes',
lucide: 'lucide',
},
},
})),

View File

@@ -0,0 +1,44 @@
import path from 'path';
import {
writeFile,
readSvgDirectory,
resetFile,
toPascalCase,
appendFile,
} from '../../../scripts/helpers';
const srcDirectory = path.join(__dirname, '../dist');
// Declare type definitions
const typeDefinitions = `\
/// <reference types="react" />
import { SVGAttributes } from 'react'
// Create interface extending SVGAttributes
export interface LucideProps extends Partial<React.SVGProps<SVGSVGElement>> {
color?: string
size?: string | number
stroke?: string | number
strokeWidth?: string | number
}
// Generated icons
`;
const ICONS_DIR = path.resolve(__dirname, '../../../icons');
const TYPES_FILE = 'lucide-react.d.ts';
resetFile(TYPES_FILE, srcDirectory);
writeFile(typeDefinitions, TYPES_FILE, srcDirectory);
const svgFiles = readSvgDirectory(ICONS_DIR);
svgFiles.forEach(svgFile => {
const iconName = path.basename(svgFile, '.svg');
const componentName = toPascalCase(iconName);
const exportTypeString = `export declare const ${componentName}: (props: LucideProps) => JSX.Element;\n`;
appendFile(exportTypeString, TYPES_FILE, srcDirectory);
});
console.log(`Generated ${TYPES_FILE} file with`, svgFiles.length, 'icons');

View File

@@ -1,8 +1,7 @@
export default ({ componentName, node }) => `
export default ({ componentName, children }) => `
import createReactComponent from '../createReactComponent';
import defaultAttributes from '../defaultAttributes';
const ${componentName} = createReactComponent('${componentName}', ['svg', defaultAttributes, ${node}]);
const ${componentName} = createReactComponent('${componentName}', ${JSON.stringify(children)});
export default ${componentName};
`;

View File

@@ -1,21 +1,22 @@
import { forwardRef, createElement } from 'react';
import PropTypes from 'prop-types';
import defaultAttributes from './defaultAttributes';
export default (iconName, [tag, attrs, children]) => {
export default (iconName, iconNode) => {
const Component = forwardRef(
({ color = 'currentColor', size = 24, strokeWidth = 2, ...rest }, ref) =>
createElement(
tag,
'svg',
{
ref,
...attrs,
...defaultAttributes,
width: size,
height: size,
stroke: color,
strokeWidth,
...rest,
},
children.map(([childTag, childAttrs]) => createElement(childTag, childAttrs)),
iconNode.map(([tag, attrs]) => createElement(tag, attrs)),
),
);

View File

@@ -0,0 +1 @@
Folder for generated icons

View File

@@ -1,5 +0,0 @@
/*
Icons exports.
Will be generated
*/

View File

@@ -1,6 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { Grid } from '..'
import { Grid } from '../src/icons'
describe('Using lucide icon components', () => {
it('should render an component', () => {

View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2020, Lucide Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,6 +1,8 @@
# Lucide Vue
Use the lucide icon library in you Vue app.
Implementation of the lucide icon library for Vue applications.
> What is lucide? Read it [here](https://github.com/lucide-icons/lucide#what-is-lucide).
## Installation

View File

@@ -7,7 +7,7 @@ module.exports = {
'^.+\\.vue$': 'vue-jest',
},
transformIgnorePatterns: [`/node_modules`],
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
snapshotSerializers: ['jest-serializer-vue'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},

View File

@@ -21,19 +21,16 @@
"vue": "^2.6.12"
},
"scripts": {
"build": "yarn clean && yarn build:move && yarn build:icons && yarn build:es && yarn build:bundles",
"clean": "rm -rf dist && rm -rf build",
"build:move": "cp -av src build",
"build:icons": "yarn --cwd ../../ build:icons --output=../packages/lucide-vue/build --templateSrc=../packages/lucide-vue/scripts/exportTemplate --noDefaultAttrs",
"build:es": "yarn --cwd ../../ babel packages/lucide-vue/build -d packages/lucide-vue/dist/esm",
"build": "yarn clean && yarn build:icons && yarn build:es && yarn build:bundles",
"clean": "rm -rf dist && rm -rf ./src/icons/*.js",
"build:icons": "yarn --cwd ../../ build:icons --output=../packages/lucide-vue/src --templateSrc=../packages/lucide-vue/scripts/exportTemplate",
"build:es": "yarn --cwd ../../ babel packages/lucide-vue/src -d packages/lucide-vue/dist/esm",
"build:bundles": "yarn --cwd ../../ rollup -c packages/lucide-vue/rollup.config.js",
"test": "jest",
"test:watch": "jest --watchAll"
},
"devDependencies": {
"@vue/test-utils": "^1.1.2",
"babel-jest": "^26.6.3",
"jest": "^26.6.3",
"jest-serializer-vue": "^2.0.2",
"vue-jest": "^3.0.7",
"vue-template-compiler": "^2.6.12"

View File

@@ -1,11 +1,11 @@
const plugins = require('../../rollup.plugins');
const pkg = require('./package.json');
import plugins from '../../rollup.plugins';
import pkg from './package.json';
const packageName = 'LucideVue';
const outputFileName = 'lucide-vue';
const rootDir = 'packages/lucide-vue'; // It runs from the root
const outputDir = `${rootDir}/dist`;
const inputs = [`${rootDir}/build/lucide-vue.js`];
const inputs = [`${rootDir}/src/lucide-vue.js`];
const bundles = [
{
format: 'umd',

View File

@@ -1,8 +1,7 @@
export default ({ componentName, node }) => `
export default ({ componentName, children }) => `
import createVueComponent from '../createVueComponent';
import defaultAttributes from '../defaultAttributes';
const ${componentName} = createVueComponent('${componentName}Icon', ['svg', defaultAttributes, ${node}]);
const ${componentName} = createVueComponent('${componentName}Icon', ${JSON.stringify(children)});
export default ${componentName};
`;

View File

@@ -1,4 +1,6 @@
export default (iconName, [tag, defaultAttrs, children]) => ({
import defaultAttributes from './defaultAttributes';
export default (iconName, iconNode) => ({
name: iconName,
functional: true,
props: {
@@ -27,13 +29,13 @@ export default (iconName, [tag, defaultAttrs, children]) => ({
},
) {
return createElement(
tag,
'svg',
{
// eslint-disable-next-line prettier/prettier
class: [defaultClass, data.class, data.staticClass, data.attrs && data.attrs.class].filter(Boolean),
style: [data.style, data.staticStyle, data.attrs && data.attrs.style].filter(Boolean),
attrs: {
...defaultAttrs,
...defaultAttributes,
width: size,
height: size,
stroke: color,
@@ -41,7 +43,7 @@ export default (iconName, [tag, defaultAttrs, children]) => ({
...data.attrs,
},
},
children.map(([childTag, childAttrs]) => createElement(childTag, { attrs: childAttrs })),
iconNode.map(([tag, attrs]) => createElement(tag, { attrs })),
);
},
});

View File

@@ -0,0 +1 @@
Folder for generated icons

View File

@@ -1,5 +0,0 @@
/*
Icons exports.
Will be generated
*/

View File

@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import { Smile } from '..'
import { Smile } from '../src/icons'
describe('Using lucide icon components', () => {
it('should render an component', () => {

1
packages/lucide/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
src/icons/*.js

View File

@@ -0,0 +1,10 @@
stats
node_modules
tests
scripts
build
src
babel.config.js
jest.config.js
rollup.config.js
yarn.error.log

15
packages/lucide/LICENSE Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2020, Lucide Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

120
packages/lucide/README.md Normal file
View File

@@ -0,0 +1,120 @@
# Lucide
Implementation of the lucide icon library for web applications.
## Installation
### Package Managers
``` bash
npm install lucide
#or
yarn add lucide
```
### CDN
``` html
<!-- Development version -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<!-- Production version -->
<script src="https://unpkg.com/lucide@latest"></script>
```
## Usage
### With unpkg
Here is a complete example with unpkg
```html
<!DOCTYPE html>
<body>
<i icon-name="volume-2" class="my-class"></i>
<i icon-name="x"></i>
<i icon-name="menu"></i>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
lucide.createIcons();
</script>
</body>
```
### With ESModules
To reduce bundle size, lucide is built to be fully treeshakable.
The `createIcons` function will search for HTMLElements with the attribute `icon-name` and replace it with the svg from the given icon name.
```html
<!-- Your HTML file -->
<i icon-name="menu"></i>
```
```js
import { createIcons, icons } from 'lucide';
// Caution, this will import all the icons and bundle them.
createIcons({icons});
// Recommended way, to include only the icons you need.
import { createIcons, Menu, ArrowRight, Globe } from 'lucide';
createIcons({
icons: {
Menu,
ArrowRight,
Globe,
},
});
```
#### Additional Options
In the `createIcons` function you can pass some extra parameters to adjust the `nameAttr` or add custom attributes like for example classes.
Here is a full example:
```js
import { createIcons } from 'lucide';
createIcons({
attrs: {
class: ['my-custom-class', 'icon'],
'stroke-width': 1,
stroke: '#333',
},
nameAttr: 'icon-name', // attribute for the icon name.
});
```
#### Treeshake the library, only use the icons you use
```js
import { createIcons, Menu, ArrowRight, Globe } from 'lucide';
createIcons({
icons: {
Menu,
ArrowRight,
Globe,
},
});
```
#### Custom Element binding
```js
import { createElement, Menu } from 'lucide';
const menuIcon = createElement(Menu); // Returns HTMLElement (svg)
// set custom attributes with browser native functions
menuIcon.setAttribute('stroke', '#333');
menuIcon.classList.add('my-icon-class');
// Append HTMLElement in webpage
const myApp = document.getElementById('app');
myApp.appendChild(menuIcon);
```

View File

@@ -0,0 +1,14 @@
const mainConfig = require('../../babel.config');
module.exports = {
presets: [
[
'@babel/env',
{
loose: true,
modules: false,
},
],
],
env: mainConfig.env,
};

View File

@@ -0,0 +1,11 @@
const esModules = ['lodash-es'].join('|');
module.exports = {
verbose: true,
roots: ['<rootDir>/src/', '<rootDir>/tests/'],
moduleFileExtensions: ['js'],
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
transform: {
'^.+\\.js$': 'babel-jest',
},
};

View File

@@ -0,0 +1,30 @@
{
"name": "lucide",
"description": "Lucide is a community-run fork of Feather Icons, open for anyone to contribute icons.",
"version": "0.14.0",
"license": "ISC",
"homepage": "https://lucide.dev",
"bugs": "https://github.com/lucide-icons/lucide/issues",
"repository": {
"type": "git",
"url": "https://github.com/lucide-icons/lucide.git",
"directory": "packages/lucide"
},
"amdName": "lucide",
"source": "build/lucide.js",
"main": "dist/cjs/lucide.js",
"main:umd": "dist/umd/lucide.js",
"module": "dist/esm/lucide.js",
"unpkg": "dist/umd/lucide.min.js",
"typings": "dist/lucide.d.ts",
"sideEffects": false,
"scripts": {
"build": "yarn clean && yarn build:icons && yarn build:es && yarn build:types && yarn build:bundles",
"clean": "rm -rf dist && rm -rf ./src/icons/*.js",
"build:icons": "yarn --cwd ../../ build:icons --output=../packages/lucide/src",
"build:es": "babel src -d dist/esm",
"build:types": "yarn --cwd ../../ babel-node packages/lucide/scripts/buildTypes.js",
"build:bundles": "rollup -c rollup.config.js",
"test": "jest"
}
}

View File

@@ -0,0 +1,41 @@
import plugins from '../../rollup.plugins';
import pkg from './package.json';
const outputFileName = pkg.name;
const outputDir = 'dist';
const inputs = ['src/lucide.js'];
const bundles = [
{
format: 'umd',
inputs,
outputDir,
minify: true,
},
{
format: 'umd',
inputs,
outputDir,
},
{
format: 'cjs',
inputs,
outputDir,
},
];
const configs = bundles
.map(({ inputs, outputDir, format, minify }) =>
inputs.map(input => ({
input,
plugins: plugins(pkg, minify),
output: {
name: outputFileName,
file: `${outputDir}/${format}/${outputFileName}${minify ? '.min' : ''}.js`,
format,
sourcemap: true,
},
})),
)
.flat();
export default configs;

View File

@@ -0,0 +1,37 @@
import path from 'path';
import { readSvgDirectory, resetFile, appendFile, toPascalCase } from '../../../scripts/helpers';
const TARGET_DIR = path.join(__dirname, '../dist');
const ICONS_DIR = path.resolve(__dirname, '../../../icons');
const TYPES_FILE_NAME = 'lucide.d.ts';
// Generates header of d.ts file include some types and functions
const typeDefinitions = `\
export type IconName = string;
export type IconNode = readonly [tag: string, object:SVGProps<SVGSVGElement>, children:IconNode?];
export type IconsObj = { [IconName]: IconNode }
export interface Attributes extends Partial<Props<Element>> {}
export function createElement(icon: IconNode): SVGSVGElement;
export function createIcons({ icons: IconsObj, nameAttr: string = 'icon-name', attrs: Attributes = {} }): VoidFunction;
export declare const icons: IconsObj;
// Generated icons
`;
resetFile(TYPES_FILE_NAME, TARGET_DIR);
appendFile(typeDefinitions, TYPES_FILE_NAME, TARGET_DIR);
const svgFiles = readSvgDirectory(ICONS_DIR);
svgFiles.forEach(svgFile => {
const nameSvg = path.basename(svgFile, '.svg');
const namePascal = toPascalCase(nameSvg);
appendFile(`export declare const ${namePascal}: IconNode;\n`, TYPES_FILE_NAME, TARGET_DIR);
});
console.log(`Generated ${TYPES_FILE_NAME} file with`, svgFiles.length, 'icons');

View File

@@ -0,0 +1,31 @@
/**
* Creates a new HTMLElement from icon node
* @param {string} tag
* @param {object} attrs
* @param {array} children
* @returns {HTMLElement}
*/
const createElement = (tag, attrs, children = []) => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tag);
Object.keys(attrs).forEach(name => {
element.setAttribute(name, attrs[name]);
});
if (children.length) {
children = children.forEach(child => {
const childElement = createElement(...child);
element.appendChild(childElement);
});
}
return element;
};
/**
* Creates a new HTMLElement from icon node
* @param {[tag: string, attrs: object, children: array]} iconNode
* @returns {HTMLElement}
*/
export default ([tag, attrs, children]) => createElement(tag, attrs, children);

View File

@@ -0,0 +1,11 @@
export default {
xmlns: 'http://www.w3.org/2000/svg',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};

View File

@@ -0,0 +1 @@
Folder for generated icons

View File

@@ -0,0 +1,37 @@
import replaceElement from './replaceElement';
import * as allIcons from './icons';
/**
* Replaces all elements with matching nameAttr with the defined icons
* @param {{ icons?: object, nameAttr?: string, attrs?: object }} options
*/
const createIcons = ({ icons = {}, nameAttr = 'icon-name', attrs = {} } = {}) => {
if (!Object.values(icons).length) {
throw new Error(
"Please provide an icons object.\nIf you want to use all the icons you can import it like:\n `import { createIcons, icons } from 'lucide';\nlucide.createIcons({icons});`",
);
}
if (typeof document === 'undefined') {
throw new Error('`createIcons()` only works in a browser environment.');
}
const elementsToReplace = document.querySelectorAll(`[${nameAttr}]`);
Array.from(elementsToReplace).forEach(element =>
replaceElement(element, { nameAttr, icons, attrs }),
);
};
export { createIcons };
/*
Create Element function export.
*/
export { default as createElement } from './createElement';
/*
Icons exports.
*/
export { allIcons as icons };
export * from './icons';

View File

@@ -0,0 +1,85 @@
import createElement from './createElement';
/**
* Get the attributes of an HTML element.
* @param {HTMLElement} element
* @returns {Object}
*/
export const getAttrs = element =>
Array.from(element.attributes).reduce((attrs, attr) => {
attrs[attr.name] = attr.value;
return attrs;
}, {});
/**
* Gets the classNames of an attributes Object.
* @param {Object} attrs
* @returns {Array}
*/
export const getClassNames = attrs => {
if (typeof attrs === 'string') return attrs;
if (!attrs || !attrs.class) return '';
if (attrs.class && typeof attrs.class === 'string') {
return attrs.class.split(' ');
}
if (attrs.class && Array.isArray(attrs.class)) {
return attrs.class;
}
return '';
};
/**
* Combines the classNames of array of classNames to a String
* @param {array} arrayOfClassnames
* @returns {string}
*/
export const combineClassNames = arrayOfClassnames => {
const classNameArray = arrayOfClassnames.flatMap(getClassNames);
return classNameArray
.map(classItem => classItem.trim())
.filter(Boolean)
.filter((value, index, self) => self.indexOf(value) === index)
.join(' ');
};
const toPascalCase = string =>
string.replace(/(\w)(\w*)(_|-|\s*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase());
/**
* ReplaceElement, replaces the given element with the created icon.
* @param {HTMLElement} element
* @param {{ nameAttr: string, icons: object, attrs: object }} options: { nameAttr, icons, attrs }
* @returns {Function}
*/
export default (element, { nameAttr, icons, attrs }) => {
const iconName = element.getAttribute(nameAttr);
const ComponentName = toPascalCase(iconName);
const iconNode = icons[ComponentName];
if (!iconNode) {
return console.warn(
`${element.outerHTML} icon name was not found in the provided icons object.`,
);
}
const elementAttrs = getAttrs(element);
const [tag, iconAttributes, children] = iconNode;
const iconAttrs = {
...iconAttributes,
'icon-name': iconName,
...attrs,
};
const classNames = combineClassNames(['lucide', elementAttrs, attrs]);
if (classNames) {
iconAttrs.class = classNames;
}
const svgElement = createElement([tag, iconAttrs, children]);
return element.parentNode.replaceChild(svgElement, element);
};

View File

@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`createIcons should add custom attributes 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"black\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" icon-name=\\"volume-2\\" class=\\"lucide icon custom-class\\"><polygon points=\\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\\"></polygon><path d=\\"M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07\\"></path></svg>"`;
exports[`createIcons should read elements from DOM and replace it with icons 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"24\\" height=\\"24\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\" stroke-linecap=\\"round\\" stroke-linejoin=\\"round\\" icon-name=\\"volume-2\\" class=\\"lucide\\"><polygon points=\\"11 5 6 9 2 9 2 15 6 15 11 19 11 5\\"></polygon><path d=\\"M19.07 4.93a10 10 0 010 14.14M15.54 8.46a5 5 0 010 7.07\\"></path></svg>"`;

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`combineClassNames should retuns a string of classNames 1`] = `"item item1 item2 item3 item4 item5 item6 item7 item8 item9"`;

View File

@@ -0,0 +1,67 @@
import * as icons from '../src/icons';
import { createIcons } from '../src/lucide';
import fs from 'fs';
import path from 'path';
import { parseSync, stringify } from 'svgson';
const ICONS_DIR = path.resolve(__dirname, '../../../icons');
const getOriginalSvg = (iconName) => {
const svgContent = fs.readFileSync(path.join(ICONS_DIR, `${iconName}.svg`), 'utf8');
const svgParsed = parseSync(svgContent);
svgParsed.attributes['icon-name'] = iconName;
svgParsed.attributes['class'] = 'lucide';
return stringify(svgParsed, { selfClose: false });
};
describe('createIcons', () => {
it('should read elements from DOM and replace it with icons', () => {
document.body.innerHTML = `<i icon-name="volume-2"></i>`;
createIcons({icons});
const svg = getOriginalSvg('volume-2');
expect(document.body.innerHTML).toBe(svg)
expect(document.body.innerHTML).toMatchSnapshot()
});
it('should customize the name attribute', () => {
document.body.innerHTML = `<i custom-name="volume-2"></i>`;
createIcons({
icons,
nameAttr: 'custom-name'
});
const hasSvg = !!document.querySelector('svg');
expect(hasSvg).toBeTruthy()
});
it('should add custom attributes', () => {
document.body.innerHTML = `<i icon-name="volume-2" class="lucide"></i>`;
const attrs = {
class: 'lucide icon custom-class',
fill: 'black',
};
createIcons({ icons, attrs });
const element = document.querySelector('svg');
const attributes = element.getAttributeNames();
const attributesAndValues = attributes.reduce((acc, item) => {
acc[item] = element.getAttribute(item);
return acc;
},{})
expect(document.body.innerHTML).toMatchSnapshot();
expect(attributesAndValues).toEqual(expect.objectContaining(attrs));
});
});

View File

@@ -0,0 +1,63 @@
import { getAttrs, getClassNames, combineClassNames } from '../src/replaceElement';
describe('getAtts', () => {
it('should returns attrbrutes of an element', () => {
const element = {
attributes: [
{
name: 'class',
value: 'item1 item2 item4',
},
{
name: 'date-name',
value: 'volume',
},
],
};
const attrs = getAttrs(element);
expect(attrs.class).toBe(element.attributes[0].value);
});
});
describe('getClassNames', () => {
it('should returns an array when giving class property of string', () => {
const elementAttrs = {
class: 'item1 item2 item3'
};
const attrs = getClassNames(elementAttrs);
expect(JSON.stringify(attrs)).toBe(JSON.stringify(['item1','item2','item3']));
});
it('should returns an array when givind class property with an array', () => {
const elementAttrs = {
class: ['item1','item2','item3']
};
const attrs = getClassNames(elementAttrs);
expect(JSON.stringify(attrs)).toBe(JSON.stringify(['item1','item2','item3']));
});
});
describe('combineClassNames', () => {
it('should retuns a string of classNames', () => {
const arrayOfClassnames = [
'item',
{
class: ['item1','item2','item3']
},
{
class: ['item4','item5','item6']
},
{
class: ['item7','item8','item9']
}
];
const combinedClassNames = combineClassNames(arrayOfClassnames);
expect(combinedClassNames).toMatchSnapshot();
});
});