mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-19 09:19:22 +01:00
Compare commits
11 Commits
main
...
package/an
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8dda3d4e7 | ||
|
|
a83fba9ecb | ||
|
|
2a9f3fa72e | ||
|
|
ae1ca07e36 | ||
|
|
818d99f41e | ||
|
|
a3e7e75b90 | ||
|
|
e851a03672 | ||
|
|
0abfa2f0d5 | ||
|
|
6c1e34df19 | ||
|
|
669f62bb64 | ||
|
|
708d5114d6 |
9
.github/ISSUE_TEMPLATE/02_bug_report.yml
vendored
9
.github/ISSUE_TEMPLATE/02_bug_report.yml
vendored
@@ -13,16 +13,19 @@ body:
|
||||
description: Which Lucide packages are affected? You may select more than one.
|
||||
options:
|
||||
- label: lucide
|
||||
- label: lucide-angular
|
||||
- label: lucide-angular (old version)
|
||||
- label: '@lucide/angular (new version)'
|
||||
- label: '@lucide/astro'
|
||||
- label: lucide-flutter
|
||||
- label: lucide-preact
|
||||
- label: lucide-react
|
||||
- label: lucide-react-native
|
||||
- label: lucide-solid
|
||||
- label: lucide-svelte
|
||||
- label: lucide-static
|
||||
- label: lucide-svelte (old version)
|
||||
- label: `@lucide/svelte (new version)`
|
||||
- label: lucide-vue
|
||||
- label: lucide-vue-next
|
||||
- label: lucide-astro
|
||||
- label: Figma plugin
|
||||
- label: source/main
|
||||
- label: other/not relevant
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/04_feature_request.yml
vendored
10
.github/ISSUE_TEMPLATE/04_feature_request.yml
vendored
@@ -13,19 +13,23 @@ body:
|
||||
description: Which Lucide project do you wish this feature were added to? You may select more than one.
|
||||
options:
|
||||
- label: lucide
|
||||
- label: lucide-angular
|
||||
- label: lucide-angular (old version)
|
||||
- label: '@lucide/angular (new version)'
|
||||
- label: '@lucide/astro'
|
||||
- label: lucide-flutter
|
||||
- label: lucide-preact
|
||||
- label: lucide-react
|
||||
- label: lucide-react-native
|
||||
- label: lucide-solid
|
||||
- label: lucide-svelte
|
||||
- label: lucide-static
|
||||
- label: lucide-svelte (old version)
|
||||
- label: `@lucide/svelte (new version)`
|
||||
- label: lucide-vue
|
||||
- label: lucide-vue-next
|
||||
- label: lucide-astro
|
||||
- label: Figma plugin
|
||||
- label: all JS packages
|
||||
- label: site
|
||||
- label: other/not relevant
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
1
.github/labeler.yml
vendored
1
.github/labeler.yml
vendored
@@ -59,6 +59,7 @@
|
||||
🅰️ angular package:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'packages/angular/*'
|
||||
- 'packages/lucide-angular/*'
|
||||
|
||||
# For changes in the lucide preact package
|
||||
|
||||
41
.github/workflows/angular.yml
vendored
Normal file
41
.github/workflows/angular.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Lucide Angular checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/angular/**
|
||||
- tools/build-icons/**
|
||||
- pnpm-lock.yaml
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @lucide/angular build
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/angular test
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -58,6 +58,7 @@ jobs:
|
||||
'lucide-preact',
|
||||
'lucide-solid',
|
||||
'lucide-svelte',
|
||||
'@lucide/angular',
|
||||
'@lucide/astro',
|
||||
'@lucide/svelte',
|
||||
]
|
||||
|
||||
38
packages/angular/.eslintrc.js
Normal file
38
packages/angular/.eslintrc.js
Normal file
@@ -0,0 +1,38 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@angular-eslint/recommended',
|
||||
'plugin:@angular-eslint/template/process-inline-templates',
|
||||
'prettier',
|
||||
],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lucide',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: ['lucide'],
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.html'],
|
||||
extends: ['plugin:@angular-eslint/template/recommended'],
|
||||
rules: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
73
packages/angular/README.md
Normal file
73
packages/angular/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/lucide-icons/lucide">
|
||||
<img src="https://lucide.dev/package-logos/lucide-angular.svg" alt="Lucide icon library for Angular applications." width="540">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Lucide icon library for Angular applications.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://www.npmjs.com/package/lucide-angular)
|
||||

|
||||
[](https://lucide.dev/license)
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://lucide.dev/guide/">About</a>
|
||||
·
|
||||
<a href="https://lucide.dev/icons/">Icons</a>
|
||||
·
|
||||
<a href="https://lucide.dev/guide/packages/lucide-angular">Documentation</a>
|
||||
·
|
||||
<a href="https://lucide.dev/license">License</a>
|
||||
</p>
|
||||
|
||||
# Lucide Angular
|
||||
|
||||
Implementation of the lucide icon library for angular applications.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add lucide-angular
|
||||
```
|
||||
|
||||
```sh
|
||||
npm install lucide-angular
|
||||
```
|
||||
|
||||
```sh
|
||||
yarn add lucide-angular
|
||||
```
|
||||
|
||||
```sh
|
||||
bun add lucide-angular
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/lucide-angular)
|
||||
|
||||
## Community
|
||||
|
||||
Join the [Discord server](https://discord.gg/EH6nSts) to chat with the maintainers and other users.
|
||||
|
||||
## License
|
||||
|
||||
Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/license).
|
||||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
|
||||
<img src="https://lucide.dev/vercel.svg" alt="Powered by Vercel" width="200" />
|
||||
</a>
|
||||
|
||||
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
|
||||
|
||||
### Awesome backers 🍺
|
||||
|
||||
<a href="https://www.scipress.io?utm_source=lucide"><img src="https://lucide.dev/sponsors/scipress.svg" width="180" alt="Scipress sponsor badge" /></a>
|
||||
<a href="https://github.com/pdfme/pdfme"><img src="https://lucide.dev/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a>
|
||||
51
packages/angular/angular.json
Normal file
51
packages/angular/angular.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"@lucide/angular": {
|
||||
"projectType": "library",
|
||||
"root": ".",
|
||||
"sourceRoot": "./src",
|
||||
"prefix": "lib",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular/build:ng-packagr",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"tsConfig": "./tsconfig.lib.prod.json"
|
||||
},
|
||||
"development": {
|
||||
"tsConfig": "./tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular/build:unit-test",
|
||||
"options": {
|
||||
"tsConfig": "./tsconfig.spec.json",
|
||||
"coverage": true,
|
||||
"coverageReporters": ["html", "lcov"],
|
||||
"coverageExclude": ["src/icons/*"],
|
||||
"coverageThresholds": {
|
||||
"statements": 80,
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"lines": 80
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"packageManager": "pnpm"
|
||||
}
|
||||
}
|
||||
7
packages/angular/ng-package.json
Normal file
7
packages/angular/ng-package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
|
||||
"dest": "./dist",
|
||||
"lib": {
|
||||
"entryFile": "./src/public-api.ts"
|
||||
}
|
||||
}
|
||||
75
packages/angular/package.json
Normal file
75
packages/angular/package.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "@lucide/angular",
|
||||
"description": "A Lucide icon library package for Angular applications.",
|
||||
"version": "0.0.1",
|
||||
"author": "SMAH1",
|
||||
"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-angular"
|
||||
},
|
||||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
},
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons",
|
||||
"build": "pnpm build:ng",
|
||||
"copy:license": "cp ../../LICENSE ./LICENSE",
|
||||
"clean": "rm -rf dist && rm -rf ./src/icons/*.ts",
|
||||
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts",
|
||||
"build:ng": "ng build --configuration production",
|
||||
"test": "ng test --no-watch",
|
||||
"test:watch": "ng test",
|
||||
"lint": "npx eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
|
||||
"e2e": "ng e2e",
|
||||
"version": "pnpm version --git-tag-version=false"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.html",
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "~21.1.0",
|
||||
"@angular-eslint/eslint-plugin": "~21.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "~21.1.0",
|
||||
"@angular-eslint/schematics": "~21.1.0",
|
||||
"@angular-eslint/template-parser": "~21.1.0",
|
||||
"@angular/build": "^21.0.3",
|
||||
"@angular/cli": "^21.0.3",
|
||||
"@angular/common": "^21.0.0",
|
||||
"@angular/compiler": "^21.0.0",
|
||||
"@angular/compiler-cli": "^21.0.0",
|
||||
"@angular/core": "^21.0.0",
|
||||
"@angular/forms": "^21.0.0",
|
||||
"@angular/platform-browser": "^21.0.0",
|
||||
"@angular/router": "^21.0.0",
|
||||
"@lucide/build-icons": "workspace:*",
|
||||
"@vitest/browser-playwright": "^4.0.16",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"angular-eslint": "21.1.0",
|
||||
"jsdom": "^27.1.0",
|
||||
"ng-packagr": "^21.0.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "~5.9.2",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "13.x - 21.x",
|
||||
"@angular/core": "13.x - 21.x"
|
||||
}
|
||||
}
|
||||
68
packages/angular/scripts/exportTemplate.mts
Normal file
68
packages/angular/scripts/exportTemplate.mts
Normal file
@@ -0,0 +1,68 @@
|
||||
import base64SVG from '@lucide/build-icons/utils/base64SVG';
|
||||
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
|
||||
|
||||
export default defineExportTemplate(async ({
|
||||
componentName,
|
||||
iconName,
|
||||
children,
|
||||
getSvg,
|
||||
deprecated,
|
||||
deprecationReason,
|
||||
aliases = [],
|
||||
toPascalCase,
|
||||
}) => {
|
||||
const svgContents = await getSvg();
|
||||
const svgBase64 = base64SVG(svgContents);
|
||||
const angularComponentName = `Lucide${componentName}`;
|
||||
const selectors = [`svg[lucide${toPascalCase(iconName)}]`];
|
||||
const aliasComponentNames: string[] = [];
|
||||
for (const alias of aliases) {
|
||||
const aliasName = typeof alias === 'string' ? alias : alias.name;
|
||||
const aliasComponentName = `Lucide${toPascalCase(aliasName)}`;
|
||||
const aliasSelector = `svg[lucide${toPascalCase(aliasName)}]`;
|
||||
if (!selectors.includes(aliasSelector)) {
|
||||
selectors.push(aliasSelector);
|
||||
}
|
||||
if (aliasComponentName !== angularComponentName && !aliasComponentNames.includes(aliasComponentName)) {
|
||||
aliasComponentNames.push(aliasComponentName);
|
||||
}
|
||||
}
|
||||
|
||||
return `\
|
||||
import { LucideIconData } from '../types';
|
||||
import { LucideIconBase } from '../lucide-icon-base';
|
||||
import { Component, signal } from '@angular/core';
|
||||
|
||||
/**
|
||||
* @component @name ${componentName}
|
||||
* @description Lucide SVG icon component, renders SVG Element with children.
|
||||
*
|
||||
* @preview  - https://lucide.dev/icons/${iconName}
|
||||
* @see https://lucide.dev/guide/packages/lucide-angular - Documentation
|
||||
*
|
||||
* @param {Object} props - Lucide icons props and any valid SVG attribute
|
||||
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
|
||||
*/
|
||||
@Component({
|
||||
selector: '${selectors.join(', ')}',
|
||||
templateUrl: '../lucide-icon.html',
|
||||
standalone: true,
|
||||
})
|
||||
export class ${angularComponentName} extends LucideIconBase {
|
||||
static iconName = '${iconName}';
|
||||
static iconData: LucideIconData = ${JSON.stringify(children)};
|
||||
override readonly iconName = signal(${angularComponentName}.iconName);
|
||||
override readonly iconData = signal(${angularComponentName}.iconData);
|
||||
}
|
||||
|
||||
${aliasComponentNames.map((aliasComponentName) => {
|
||||
return `
|
||||
/**
|
||||
* @deprecated
|
||||
* @see ${angularComponentName}
|
||||
*/
|
||||
export const ${aliasComponentName} = ${angularComponentName};
|
||||
`;
|
||||
}).join(`\n\n`)}
|
||||
`;
|
||||
});
|
||||
11
packages/angular/src/default-attributes.ts
Normal file
11
packages/angular/src/default-attributes.ts
Normal 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',
|
||||
};
|
||||
25
packages/angular/src/lucide-config.spec.ts
Normal file
25
packages/angular/src/lucide-config.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { LUCIDE_CONFIG, lucideDefaultConfig, provideLucideConfig } from './lucide-config';
|
||||
|
||||
describe('Lucide config', () => {
|
||||
describe('LUCIDE_CONFIG', () => {
|
||||
it('should use default', () => {
|
||||
expect(TestBed.inject(LUCIDE_CONFIG)).toBe(lucideDefaultConfig);
|
||||
});
|
||||
});
|
||||
describe('provideLucideConfig', () => {
|
||||
it('should use defaults', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideLucideConfig({
|
||||
size: 18,
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(TestBed.inject(LUCIDE_CONFIG)).toEqual({
|
||||
...lucideDefaultConfig,
|
||||
size: 18,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
38
packages/angular/src/lucide-config.ts
Normal file
38
packages/angular/src/lucide-config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { InjectionToken, Provider } from '@angular/core';
|
||||
|
||||
/**
|
||||
* A configuration service for Lucide icon components.
|
||||
*
|
||||
* You can inject this service, typically in AppComponent, and customize its property values in
|
||||
* order to provide default values for all the icons used in the application.
|
||||
*/
|
||||
export interface LucideConfig {
|
||||
color: string;
|
||||
size: number;
|
||||
strokeWidth: number;
|
||||
absoluteStrokeWidth: boolean;
|
||||
}
|
||||
|
||||
export const lucideDefaultConfig: LucideConfig = {
|
||||
color: 'currentColor',
|
||||
size: 24,
|
||||
strokeWidth: 2,
|
||||
absoluteStrokeWidth: false,
|
||||
};
|
||||
|
||||
export const LUCIDE_CONFIG = new InjectionToken<LucideConfig>(
|
||||
'Lucide icon config',
|
||||
{
|
||||
factory: () => lucideDefaultConfig,
|
||||
},
|
||||
);
|
||||
|
||||
export function provideLucideConfig(config: Partial<LucideConfig>): Provider {
|
||||
return {
|
||||
provide: LUCIDE_CONFIG,
|
||||
useValue: {
|
||||
...lucideDefaultConfig,
|
||||
...config,
|
||||
},
|
||||
};
|
||||
}
|
||||
114
packages/angular/src/lucide-icon-base.ts
Normal file
114
packages/angular/src/lucide-icon-base.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
input,
|
||||
Renderer2,
|
||||
Signal,
|
||||
} from '@angular/core';
|
||||
import { LUCIDE_CONFIG } from './lucide-config';
|
||||
import { LucideIconData, Nullable } from './types';
|
||||
import defaultAttributes from './default-attributes';
|
||||
import { formatFixed } from './utils/format-fixed';
|
||||
import { toKebabCase } from './utils/to-kebab-case';
|
||||
|
||||
function transformNumericStringInput(
|
||||
value: Nullable<string | number>,
|
||||
defaultValue: number,
|
||||
): number {
|
||||
if (typeof value === 'string') {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
if (isNaN(parsedValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'svg[lucideIcon]',
|
||||
templateUrl: './lucide-icon.html',
|
||||
host: {
|
||||
...defaultAttributes,
|
||||
class: 'lucide',
|
||||
'[attr.width]': 'size().toString(10)',
|
||||
'[attr.height]': 'size().toString(10)',
|
||||
'[attr.stroke]': 'color()',
|
||||
'[attr.stroke-width]': 'computedStrokeWidth()',
|
||||
'[attr.aria-hidden]': 'ariaHidden()',
|
||||
},
|
||||
})
|
||||
export abstract class LucideIconBase {
|
||||
abstract iconName: Signal<Nullable<string>>;
|
||||
abstract iconData: Signal<Nullable<LucideIconData>>;
|
||||
protected readonly iconConfig = inject(LUCIDE_CONFIG);
|
||||
protected readonly elRef = inject(ElementRef);
|
||||
protected readonly renderer = inject(Renderer2);
|
||||
readonly title = input<Nullable<string>>();
|
||||
readonly ariaHidden = computed(() => {
|
||||
return !this.title();
|
||||
});
|
||||
readonly size = input(this.iconConfig.size, {
|
||||
transform: (value: Nullable<string | number>) =>
|
||||
transformNumericStringInput(value, this.iconConfig.size),
|
||||
});
|
||||
readonly color = input(this.iconConfig.color, {
|
||||
transform: (value: Nullable<string>) => value ?? this.iconConfig.color,
|
||||
});
|
||||
readonly strokeWidth = input(this.iconConfig.strokeWidth, {
|
||||
transform: (value: Nullable<string | number>) =>
|
||||
transformNumericStringInput(value, this.iconConfig.strokeWidth),
|
||||
});
|
||||
readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, {
|
||||
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.absoluteStrokeWidth,
|
||||
});
|
||||
protected readonly computedStrokeWidth = computed(() => {
|
||||
const strokeWidth = this.strokeWidth();
|
||||
const size = this.size();
|
||||
return this.absoluteStrokeWidth()
|
||||
? formatFixed(strokeWidth / (size / 24))
|
||||
: strokeWidth.toString(10);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect((onCleanup) => {
|
||||
const icon = this.iconData();
|
||||
if (icon) {
|
||||
const elements = icon.map(([name, attrs]) => {
|
||||
const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg');
|
||||
for (const [name, value] of Object.entries(attrs)) {
|
||||
this.renderer.setAttribute(
|
||||
element,
|
||||
name,
|
||||
typeof value === 'number' ? value.toString(10) : value,
|
||||
);
|
||||
}
|
||||
this.renderer.appendChild(this.elRef.nativeElement, element);
|
||||
return element;
|
||||
});
|
||||
onCleanup(() => {
|
||||
for (const element of elements) {
|
||||
this.renderer.removeChild(this.elRef.nativeElement, element);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
effect((onCleanup) => {
|
||||
const name = this.iconName();
|
||||
if (name) {
|
||||
const cssClass = `lucide-${toKebabCase(name)}`;
|
||||
this.renderer.addClass(this.elRef.nativeElement, cssClass);
|
||||
onCleanup(() => {
|
||||
this.renderer.removeClass(this.elRef.nativeElement, cssClass);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
4
packages/angular/src/lucide-icon.html
Normal file
4
packages/angular/src/lucide-icon.html
Normal file
@@ -0,0 +1,4 @@
|
||||
@if (title(); as titleValue) {
|
||||
<title>{{ titleValue }}</title>
|
||||
}
|
||||
<ng-content />
|
||||
243
packages/angular/src/lucide-icon.spec.ts
Normal file
243
packages/angular/src/lucide-icon.spec.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { Component, input, inputBinding, signal, WritableSignal } from '@angular/core';
|
||||
import { LucideIcon } from './lucide-icon';
|
||||
import { LucideIconData, LucideIconInput } from './types';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideLucideIcons } from './lucide-icons';
|
||||
import { LucideActivity } from './icons/activity';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
template: `@if (icon(); as iconData) {
|
||||
<svg [lucideIcon]="iconData">
|
||||
<rect x="1" y="1" width="22" height="22" />
|
||||
</svg>
|
||||
}`,
|
||||
imports: [LucideIcon],
|
||||
})
|
||||
class TestHostComponent {
|
||||
readonly icon = input<LucideIconData>();
|
||||
}
|
||||
|
||||
describe('LucideIcon', () => {
|
||||
let component: LucideIcon;
|
||||
let fixture: ComponentFixture<LucideIcon>;
|
||||
let icon: WritableSignal<LucideIconInput | null | undefined>;
|
||||
let name: WritableSignal<string | undefined>;
|
||||
let title: WritableSignal<string | undefined>;
|
||||
let color: WritableSignal<string | undefined>;
|
||||
let size: WritableSignal<string | number | undefined>;
|
||||
let strokeWidth: WritableSignal<string | number | undefined>;
|
||||
let absoluteStrokeWidth: WritableSignal<boolean | undefined>;
|
||||
const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr);
|
||||
const testIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
|
||||
const testIcon2: LucideIconData = [
|
||||
['circle', { cx: 12, cy: 12, r: 8 }],
|
||||
['polyline', { points: '1 1 22 22' }],
|
||||
];
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideLucideIcons({ demo: testIcon })],
|
||||
});
|
||||
icon = signal('demo');
|
||||
name = signal(undefined);
|
||||
title = signal(undefined);
|
||||
color = signal(undefined);
|
||||
size = signal(undefined);
|
||||
strokeWidth = signal(undefined);
|
||||
absoluteStrokeWidth = signal(undefined);
|
||||
fixture = TestBed.createComponent(LucideIcon, {
|
||||
inferTagName: true,
|
||||
bindings: [
|
||||
inputBinding('lucideIcon', icon),
|
||||
inputBinding('name', name),
|
||||
inputBinding('title', title),
|
||||
inputBinding('color', color),
|
||||
inputBinding('size', size),
|
||||
inputBinding('strokeWidth', strokeWidth),
|
||||
inputBinding('absoluteStrokeWidth', absoluteStrokeWidth),
|
||||
],
|
||||
});
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
icon.set(testIcon2);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.innerHTML).toBe(
|
||||
'<!--container--><circle cx="12" cy="12" r="8"></circle><polyline points="1 1 22 22"></polyline>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove children on change', () => {
|
||||
icon.set(null);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.innerHTML).toBe('<!--container-->');
|
||||
});
|
||||
|
||||
describe('iconInput', () => {
|
||||
it('should support LucideIconData input', () => {
|
||||
icon.set(testIcon);
|
||||
name.set('custom-name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.iconData()).toBe(testIcon);
|
||||
expect(component.iconName()).toBe('custom-name');
|
||||
expect(fixture.nativeElement.innerHTML).toBe(
|
||||
'<!--container--><polyline points="1 1 22 22"></polyline>',
|
||||
);
|
||||
});
|
||||
it('should support LucideIconComponentType input', () => {
|
||||
icon.set(LucideActivity);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.iconData()).toBe(LucideActivity.iconData);
|
||||
expect(component.iconName()).toBe(LucideActivity.iconName);
|
||||
});
|
||||
it('should support string icon name', () => {
|
||||
icon.set('demo');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.iconData()).toBe(testIcon);
|
||||
expect(component.iconName()).toBe('demo');
|
||||
});
|
||||
it('should throw error if no icon founds', () => {
|
||||
icon.set('invalid');
|
||||
expect(() => fixture.detectChanges()).toThrowError(`Unable to resolve icon 'invalid'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('class', () => {
|
||||
it('should add all classes', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('class')).toBe('lucide lucide-demo');
|
||||
});
|
||||
it('should add class from name, even if icon has name', () => {
|
||||
icon.set(LucideActivity);
|
||||
name.set('custom-name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getSvgAttribute('class')).toBe('lucide lucide-custom-name');
|
||||
});
|
||||
it('should add class icon if available', () => {
|
||||
icon.set(LucideActivity);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getSvgAttribute('class')).toBe('lucide lucide-activity');
|
||||
});
|
||||
it('should remove class on change', () => {
|
||||
icon.set(null);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('class')).toBe('lucide');
|
||||
});
|
||||
});
|
||||
|
||||
describe('color', () => {
|
||||
it('should default to currentColor', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke')).toBe('currentColor');
|
||||
});
|
||||
it('should set color', () => {
|
||||
color.set('red');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke')).toBe('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('size', () => {
|
||||
it('should default to 24', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('width')).toBe('24');
|
||||
expect(getSvgAttribute('height')).toBe('24');
|
||||
});
|
||||
it('should set size', () => {
|
||||
size.set(12);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('width')).toBe('12');
|
||||
expect(getSvgAttribute('height')).toBe('12');
|
||||
});
|
||||
it('should allow string size', () => {
|
||||
size.set('18');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('width')).toBe('18');
|
||||
expect(getSvgAttribute('height')).toBe('18');
|
||||
});
|
||||
it('should use default on invalid string', () => {
|
||||
size.set('large');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('width')).toBe('24');
|
||||
expect(getSvgAttribute('height')).toBe('24');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strokeWidth', () => {
|
||||
it('should default to 2', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should set stroke width', () => {
|
||||
strokeWidth.set(1.41);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1.41');
|
||||
});
|
||||
it('should allow string stroke width', () => {
|
||||
strokeWidth.set('1px');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('absoluteStrokeWidth', () => {
|
||||
it('should not adjust stroke width', () => {
|
||||
strokeWidth.set(2);
|
||||
size.set(12);
|
||||
absoluteStrokeWidth.set(false);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('2');
|
||||
});
|
||||
it('should adjust stroke width', () => {
|
||||
strokeWidth.set(2);
|
||||
size.set(12);
|
||||
absoluteStrokeWidth.set(true);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe('4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('title', () => {
|
||||
it('should set title if provided', () => {
|
||||
title.set('Foobar');
|
||||
fixture.detectChanges();
|
||||
const titleEl = fixture.debugElement.query(By.css('title')).nativeElement;
|
||||
expect(titleEl).toBeDefined();
|
||||
expect(titleEl.textContent).toBe('Foobar');
|
||||
});
|
||||
it('should not set aria-hidden when title is set', () => {
|
||||
title.set('Foobar');
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('aria-hidden')).toBeUndefined;
|
||||
});
|
||||
it('should set aria-hidden if no title is provided', () => {
|
||||
title.set(undefined);
|
||||
fixture.detectChanges();
|
||||
expect(getSvgAttribute('aria-hidden')).toBeUndefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('content projection', () => {
|
||||
it('should project content', () => {
|
||||
const hostFixture = TestBed.createComponent(TestHostComponent);
|
||||
hostFixture.componentRef.setInput('icon', testIcon);
|
||||
hostFixture.detectChanges();
|
||||
hostFixture.componentRef.setInput('icon', testIcon2);
|
||||
hostFixture.detectChanges();
|
||||
const rect = hostFixture.debugElement.query(By.css('rect')).nativeElement;
|
||||
expect(rect).toBeInstanceOf(SVGElement);
|
||||
expect(rect.outerHTML).toBe('<rect x="1" y="1" width="22" height="22"></rect>');
|
||||
});
|
||||
});
|
||||
});
|
||||
62
packages/angular/src/lucide-icon.ts
Normal file
62
packages/angular/src/lucide-icon.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { isLucideIconComponent, isLucideIconData, LucideIconInput } from './types';
|
||||
import { LucideIconBase } from './lucide-icon-base';
|
||||
import { LUCIDE_ICONS } from './lucide-icons';
|
||||
import { LucideIconData } from './types';
|
||||
import { toKebabCase } from './utils/to-kebab-case';
|
||||
|
||||
interface LucideResolvedIcon {
|
||||
name?: string | null;
|
||||
data: LucideIconData;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'svg[lucideIcon]',
|
||||
templateUrl: './lucide-icon.html',
|
||||
standalone: true,
|
||||
})
|
||||
export class LucideIcon extends LucideIconBase {
|
||||
protected readonly icons = inject(LUCIDE_ICONS);
|
||||
readonly name = input<string | null>();
|
||||
readonly iconInput = input.required<LucideIconInput | null>({
|
||||
alias: 'lucideIcon',
|
||||
});
|
||||
readonly resolvedIcon = computed<LucideResolvedIcon | null>(() => {
|
||||
return this.resolveIcon(this.name(), this.iconInput());
|
||||
});
|
||||
override readonly iconName = computed(() => {
|
||||
return this.resolvedIcon()?.name;
|
||||
});
|
||||
override readonly iconData = computed(() => {
|
||||
return this.resolvedIcon()?.data;
|
||||
});
|
||||
|
||||
protected resolveIcon(
|
||||
name: string | null | undefined,
|
||||
icon: LucideIconInput | null | undefined,
|
||||
): LucideResolvedIcon | null {
|
||||
if (isLucideIconData(icon)) {
|
||||
return {
|
||||
name,
|
||||
data: icon,
|
||||
};
|
||||
} else if (isLucideIconComponent(icon)) {
|
||||
return {
|
||||
name: name ?? icon.iconName,
|
||||
data: icon.iconData,
|
||||
};
|
||||
} else if (typeof icon === 'string') {
|
||||
const name = toKebabCase(icon);
|
||||
if (name in this.icons) {
|
||||
return {
|
||||
name,
|
||||
data: this.icons[name],
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unable to resolve icon '${icon}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
44
packages/angular/src/lucide-icons.spec.ts
Normal file
44
packages/angular/src/lucide-icons.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { LUCIDE_ICONS, provideLucideIcons } from './lucide-icons';
|
||||
import { LucideIconData } from './types';
|
||||
import { LucideActivity } from './icons/activity';
|
||||
import { LucideCircle } from './icons/circle';
|
||||
import { LucideSquareX } from './icons/square-x';
|
||||
|
||||
describe('Lucide icons', () => {
|
||||
describe('LUCIDE_ICONS', () => {
|
||||
it('should default to empty map', () => {
|
||||
expect(TestBed.inject(LUCIDE_ICONS)).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('provideLucideIcons', () => {
|
||||
const mockIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
|
||||
const mockIcon2: LucideIconData = [['circle', { cx: 12, cy: 12, r: 8 }]];
|
||||
it('should accept dictionary of icons', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideLucideIcons({
|
||||
DemoIcon: mockIcon,
|
||||
MockIcon: mockIcon2,
|
||||
TestIcon: LucideActivity,
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(TestBed.inject(LUCIDE_ICONS)).toEqual({
|
||||
'demo-icon': mockIcon,
|
||||
'mock-icon': mockIcon2,
|
||||
[LucideActivity.iconName]: LucideActivity.iconData,
|
||||
});
|
||||
});
|
||||
it('should accept list of icon components', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideLucideIcons([LucideActivity, LucideSquareX, LucideCircle])],
|
||||
});
|
||||
expect(TestBed.inject(LUCIDE_ICONS)).toEqual({
|
||||
[LucideActivity.iconName]: LucideActivity.iconData,
|
||||
[LucideSquareX.iconName]: LucideSquareX.iconData,
|
||||
[LucideCircle.iconName]: LucideCircle.iconData,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
34
packages/angular/src/lucide-icons.ts
Normal file
34
packages/angular/src/lucide-icons.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { InjectionToken, Provider } from '@angular/core';
|
||||
import { LucideIconData, LucideIcons } from './types';
|
||||
import { isLucideIconComponent, LucideIconComponentType } from './types';
|
||||
import { toKebabCase } from './utils/to-kebab-case';
|
||||
|
||||
export const LUCIDE_ICONS = new InjectionToken<LucideIcons>('Lucide icons', {
|
||||
factory: () => ({}),
|
||||
});
|
||||
|
||||
export function provideLucideIcons(
|
||||
icons: Record<string, LucideIconData | LucideIconComponentType> | Array<LucideIconComponentType>,
|
||||
): Provider {
|
||||
if (Array.isArray(icons)) {
|
||||
return {
|
||||
provide: LUCIDE_ICONS,
|
||||
useValue: icons.reduce((acc, icon) => {
|
||||
acc[toKebabCase(icon.iconName)] = icon.iconData;
|
||||
return acc;
|
||||
}, {} as LucideIcons),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
provide: LUCIDE_ICONS,
|
||||
useValue: Object.entries(icons).reduce((acc, [name, icon]) => {
|
||||
if (isLucideIconComponent(icon)) {
|
||||
acc[icon.iconName] = icon.iconData;
|
||||
} else {
|
||||
acc[toKebabCase(name)] = icon;
|
||||
}
|
||||
return acc;
|
||||
}, {} as LucideIcons),
|
||||
};
|
||||
}
|
||||
}
|
||||
8
packages/angular/src/public-api.ts
Normal file
8
packages/angular/src/public-api.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as icons from './icons/lucide-angular';
|
||||
|
||||
export * from './lucide-config';
|
||||
export * from './lucide-icon';
|
||||
export * from './lucide-icons';
|
||||
export * from './types';
|
||||
export * from './icons/lucide-angular';
|
||||
export { icons };
|
||||
34
packages/angular/src/types.ts
Normal file
34
packages/angular/src/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Signal, Type } from '@angular/core';
|
||||
|
||||
type HtmlAttributes = { [key: string]: string | number };
|
||||
export type LucideIconNode = readonly [string, HtmlAttributes];
|
||||
export type LucideIconData = readonly LucideIconNode[];
|
||||
export type LucideIcons = { [key: string]: LucideIconData };
|
||||
|
||||
export interface LucideIconComponentInterface {
|
||||
iconName: Signal<Nullable<string>>;
|
||||
iconData: Signal<Nullable<LucideIconData>>;
|
||||
}
|
||||
|
||||
export type LucideIconComponentType = Type<LucideIconComponentInterface> & {
|
||||
iconName: string;
|
||||
iconData: LucideIconData;
|
||||
};
|
||||
|
||||
export function isLucideIconData(icon: unknown): icon is LucideIconData {
|
||||
return Array.isArray(icon);
|
||||
}
|
||||
|
||||
export function isLucideIconComponent(icon: unknown): icon is LucideIconComponentType {
|
||||
return (
|
||||
icon instanceof Type &&
|
||||
'iconData' in icon &&
|
||||
Array.isArray(icon.iconData) &&
|
||||
'iconName' in icon &&
|
||||
typeof icon.iconName === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export type LucideIconInput = LucideIconComponentType | LucideIconData | string;
|
||||
|
||||
export type Nullable<T> = T | null | undefined;
|
||||
3
packages/angular/src/utils/format-fixed.ts
Normal file
3
packages/angular/src/utils/format-fixed.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function formatFixed(number: number, decimals = 3): string {
|
||||
return parseFloat(number.toFixed(decimals)).toString(10);
|
||||
}
|
||||
2
packages/angular/src/utils/to-kebab-case.ts
Normal file
2
packages/angular/src/utils/to-kebab-case.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const toKebabCase = (name: string) =>
|
||||
name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
||||
40
packages/angular/tsconfig.json
Normal file
40
packages/angular/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@lucide/angular": [
|
||||
"./dist"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
packages/angular/tsconfig.lib.json
Normal file
18
packages/angular/tsconfig.lib.json
Normal file
@@ -0,0 +1,18 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/lib",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
11
packages/angular/tsconfig.lib.prod.json
Normal file
11
packages/angular/tsconfig.lib.prod.json
Normal file
@@ -0,0 +1,11 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.lib.json",
|
||||
"compilerOptions": {
|
||||
"declarationMap": false
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"compilationMode": "partial"
|
||||
}
|
||||
}
|
||||
15
packages/angular/tsconfig.spec.json
Normal file
15
packages/angular/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"vitest/globals"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
101
packages/lucide-angular/src/lib/lucide-icon.component.spec.ts
Normal file
101
packages/lucide-angular/src/lib/lucide-icon.component.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { formatFixed, LucideIcon } from './lucide-icon.component';
|
||||
import defaultAttributes from '../icons/constants/default-attributes';
|
||||
import { LucideIconData } from '../icons/types';
|
||||
|
||||
describe('LucideAngularComponent', () => {
|
||||
let testHostComponent: TestHostComponent;
|
||||
let testHostFixture: ComponentFixture<TestHostComponent>;
|
||||
const getSvgAttribute = (attr: string) =>
|
||||
testHostFixture.nativeElement.querySelector('svg').getAttribute(attr);
|
||||
const testIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [LucideIcon, TestHostComponent],
|
||||
imports: [],
|
||||
}).compileComponents();
|
||||
testHostFixture = TestBed.createComponent(TestHostComponent);
|
||||
testHostComponent = testHostFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
testHostFixture.detectChanges();
|
||||
expect(testHostComponent).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should add all classes', () => {
|
||||
testHostFixture.detectChanges();
|
||||
|
||||
expect(getSvgAttribute('class')).toBe('lucide lucide-demo my-icon');
|
||||
});
|
||||
|
||||
it('should set color', () => {
|
||||
const color = 'red';
|
||||
testHostComponent.setColor(color);
|
||||
testHostFixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke')).toBe(color);
|
||||
});
|
||||
|
||||
it('should set size', () => {
|
||||
const size = 12;
|
||||
testHostComponent.setSize(size);
|
||||
testHostFixture.detectChanges();
|
||||
expect(getSvgAttribute('width')).toBe(size.toString(10));
|
||||
});
|
||||
|
||||
it('should set stroke width', () => {
|
||||
const strokeWidth = 1.41;
|
||||
testHostComponent.setStrokeWidth(strokeWidth);
|
||||
testHostFixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe(strokeWidth.toString(10));
|
||||
});
|
||||
|
||||
it('should adjust stroke width', () => {
|
||||
const strokeWidth = 2;
|
||||
const size = 12;
|
||||
testHostComponent.setStrokeWidth(strokeWidth);
|
||||
testHostComponent.setSize(12);
|
||||
testHostComponent.setAbsoluteStrokeWidth(true);
|
||||
testHostFixture.detectChanges();
|
||||
expect(getSvgAttribute('stroke-width')).toBe(
|
||||
formatFixed(strokeWidth / (size / defaultAttributes.height))
|
||||
);
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: 'lucide-spec-host-component',
|
||||
template: ` <i-lucide
|
||||
name="demo"
|
||||
[img]="testIcon"
|
||||
class="my-icon"
|
||||
[color]="color"
|
||||
[size]="size"
|
||||
[strokeWidth]="strokeWidth"
|
||||
[absoluteStrokeWidth]="absoluteStrokeWidth"
|
||||
></i-lucide>`,
|
||||
})
|
||||
class TestHostComponent {
|
||||
color?: string;
|
||||
size?: number;
|
||||
strokeWidth?: number;
|
||||
absoluteStrokeWidth = true;
|
||||
readonly testIcon = testIcon;
|
||||
|
||||
setColor(color: string): void {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
setSize(size: number): void {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
setStrokeWidth(strokeWidth: number): void {
|
||||
this.strokeWidth = strokeWidth;
|
||||
}
|
||||
|
||||
setAbsoluteStrokeWidth(absoluteStrokeWidth: boolean): void {
|
||||
this.absoluteStrokeWidth = absoluteStrokeWidth;
|
||||
}
|
||||
}
|
||||
});
|
||||
213
packages/lucide-angular/src/lib/lucide-icon.component.ts
Normal file
213
packages/lucide-angular/src/lib/lucide-icon.component.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
Inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
SimpleChange,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
import { LucideIconData } from '../icons/types';
|
||||
import defaultAttributes from '../icons/constants/default-attributes';
|
||||
import { LucideIconConfig } from './lucide-icon.config';
|
||||
|
||||
interface TypedChange<T> extends SimpleChange {
|
||||
previousValue: T;
|
||||
currentValue: T;
|
||||
}
|
||||
|
||||
type SvgAttributes = { [key: string]: string | number };
|
||||
|
||||
type LucideAngularComponentChanges = {
|
||||
name?: TypedChange<string | LucideIconData>;
|
||||
icon?: TypedChange<LucideIconData | undefined>;
|
||||
color?: TypedChange<string>;
|
||||
size?: TypedChange<number>;
|
||||
strokeWidth?: TypedChange<number>;
|
||||
absoluteStrokeWidth?: TypedChange<boolean>;
|
||||
class: TypedChange<string>;
|
||||
};
|
||||
|
||||
export function formatFixed(number: number, decimals = 3): string {
|
||||
return parseFloat(number.toFixed(decimals)).toString(10);
|
||||
}
|
||||
|
||||
export type LucideIconComponentType = Type<LucideIcon> & { iconData: LucideIconData; name: string };
|
||||
|
||||
function isLucideIconComponent(icon: unknown): icon is LucideIconComponentType {
|
||||
return (
|
||||
icon instanceof Type &&
|
||||
'iconData' in icon &&
|
||||
Array.isArray(icon.iconData) &&
|
||||
'iconName' in icon &&
|
||||
typeof icon.iconName === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
@Component({
|
||||
// eslint-disable-next-line @angular-eslint/component-selector
|
||||
selector: 'svg[lucideIcon]',
|
||||
template: '<ng-content></ng-content>',
|
||||
standalone: true,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/component-class-suffix
|
||||
export class LucideIcon implements OnInit, OnChanges {
|
||||
@Input() class?: string;
|
||||
_name?: string;
|
||||
@Input() set name(name: string | undefined) {
|
||||
this._name = name;
|
||||
}
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
_icon?: LucideIconData | LucideIconComponentType | null;
|
||||
@Input('lucideIcon') set icon(icon: LucideIconData | LucideIconComponentType | null | undefined) {
|
||||
this._icon = icon;
|
||||
}
|
||||
get icon() {
|
||||
return this._icon;
|
||||
}
|
||||
@Input() color?: string;
|
||||
@Input() absoluteStrokeWidth = false;
|
||||
defaultSize: number;
|
||||
|
||||
constructor(
|
||||
@Inject(ElementRef) protected elem: ElementRef,
|
||||
@Inject(Renderer2) protected renderer: Renderer2,
|
||||
@Inject(ChangeDetectorRef) protected changeDetector: ChangeDetectorRef,
|
||||
@Inject(LucideIconConfig) protected iconConfig: LucideIconConfig
|
||||
) {
|
||||
this.defaultSize = defaultAttributes.height;
|
||||
}
|
||||
|
||||
_size?: number;
|
||||
|
||||
get size(): number {
|
||||
return this._size ?? this.iconConfig.size;
|
||||
}
|
||||
|
||||
@Input() set size(value: string | number | undefined) {
|
||||
if (value) {
|
||||
this._size = this.parseNumber(value);
|
||||
} else {
|
||||
delete this._size;
|
||||
}
|
||||
}
|
||||
|
||||
_strokeWidth?: number;
|
||||
|
||||
get strokeWidth(): number {
|
||||
return this._strokeWidth ?? this.iconConfig.strokeWidth;
|
||||
}
|
||||
|
||||
@Input() set strokeWidth(value: string | number | undefined) {
|
||||
if (value) {
|
||||
this._strokeWidth = this.parseNumber(value);
|
||||
} else {
|
||||
delete this._strokeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.buildIcon();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: LucideAngularComponentChanges): void {
|
||||
if (
|
||||
changes.name ||
|
||||
changes.icon ||
|
||||
changes.color ||
|
||||
changes.size ||
|
||||
changes.absoluteStrokeWidth ||
|
||||
changes.strokeWidth ||
|
||||
changes.class
|
||||
) {
|
||||
this.buildIcon();
|
||||
}
|
||||
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
|
||||
buildIcon(): void {
|
||||
this.color = this.color ?? this.iconConfig.color;
|
||||
this.size = this.parseNumber(this.size ?? this.iconConfig.size);
|
||||
this.strokeWidth = this.parseNumber(this.strokeWidth ?? this.iconConfig.strokeWidth);
|
||||
this.absoluteStrokeWidth = this.absoluteStrokeWidth ?? this.iconConfig.absoluteStrokeWidth;
|
||||
console.log('Hello, my name is ', this.name, ' my icon is ', this.icon);
|
||||
if (this.icon) {
|
||||
this.replaceElement(isLucideIconComponent(this.icon) ? this.icon.iconData : this.icon);
|
||||
}
|
||||
}
|
||||
|
||||
replaceElement(img: LucideIconData): void {
|
||||
const attributes = {
|
||||
...defaultAttributes,
|
||||
width: this.size,
|
||||
height: this.size,
|
||||
stroke: this.color ?? this.iconConfig.color,
|
||||
'stroke-width': this.absoluteStrokeWidth
|
||||
? formatFixed(this.strokeWidth / (this.size / this.defaultSize))
|
||||
: this.strokeWidth.toString(10),
|
||||
};
|
||||
const icoElement = this.elem.nativeElement;
|
||||
for (const [name, value] of Object.entries(attributes)) {
|
||||
icoElement.setAttribute(name, value);
|
||||
}
|
||||
icoElement.classList.add('lucide');
|
||||
if (typeof this.name === 'string') {
|
||||
icoElement.classList.add(`lucide-${this.name.replace('_', '-')}`);
|
||||
}
|
||||
if (this.class) {
|
||||
icoElement.classList.add(
|
||||
...this.class
|
||||
.split(/ /)
|
||||
.map((a) => a.trim())
|
||||
.filter((a) => a.length > 0)
|
||||
);
|
||||
}
|
||||
for (const child of icoElement.children) {
|
||||
this.renderer.removeChild(this.elem.nativeElement, child);
|
||||
}
|
||||
for (const node of img) {
|
||||
const childElement = this.createElement(node);
|
||||
this.renderer.appendChild(icoElement, childElement);
|
||||
}
|
||||
}
|
||||
|
||||
protected parseNumber(value: string | number): number {
|
||||
if (typeof value === 'string') {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
if (isNaN(parsedValue)) {
|
||||
throw new Error(`${value} is not numeric.`);
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected createElement([tag, attrs, children = []]: readonly [
|
||||
string,
|
||||
SvgAttributes,
|
||||
LucideIconData?
|
||||
]) {
|
||||
const element = this.renderer.createElement(tag, 'http://www.w3.org/2000/svg');
|
||||
|
||||
Object.keys(attrs).forEach((name) => {
|
||||
const attrValue: string =
|
||||
typeof attrs[name] === 'string' ? (attrs[name] as string) : attrs[name].toString(10);
|
||||
this.renderer.setAttribute(element, name, attrValue);
|
||||
});
|
||||
|
||||
if (children.length) {
|
||||
children.forEach((child) => {
|
||||
const childElement = this.createElement(child);
|
||||
this.renderer.appendChild(element, childElement);
|
||||
});
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
6300
pnpm-lock.yaml
generated
6300
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ export default async function generateExportFile(
|
||||
} else if (exportModuleNameCasing === 'pascal') {
|
||||
componentName = toPascalCase(iconName);
|
||||
}
|
||||
const importString = `export { default as ${componentName} } from './${iconName}${iconFileExtension}';\n`;
|
||||
const importString = `export * from './${iconName}${iconFileExtension}';\n`;
|
||||
return appendFile(importString, fileName, outputDirectory);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ function generateIconFiles({
|
||||
]);
|
||||
|
||||
const getSvg = () => readSvg(`${iconName}.svg`, iconsDir);
|
||||
const { deprecated = false, toBeRemovedInVersion = undefined } = iconMetaData[iconName];
|
||||
const {
|
||||
deprecated = false,
|
||||
toBeRemovedInVersion = undefined,
|
||||
aliases,
|
||||
} = iconMetaData[iconName];
|
||||
const deprecationReason = deprecated
|
||||
? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', {
|
||||
componentName,
|
||||
@@ -64,6 +68,8 @@ function generateIconFiles({
|
||||
getSvg,
|
||||
deprecated,
|
||||
deprecationReason,
|
||||
aliases,
|
||||
toPascalCase,
|
||||
});
|
||||
|
||||
const output = pretty
|
||||
@@ -71,7 +77,7 @@ function generateIconFiles({
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
printWidth: 100,
|
||||
parser: 'babel',
|
||||
parser: iconFileExtension.endsWith('.ts') ? 'babel-ts' : 'babel',
|
||||
})
|
||||
: elementTemplate;
|
||||
|
||||
@@ -81,7 +87,7 @@ function generateIconFiles({
|
||||
const output = `export { default } from "./${iconName}${iconFileExtension}";\n`;
|
||||
const location = path.join(
|
||||
iconsDistDirectory,
|
||||
`${iconName}${separateIconFileExportExtension ?? iconFileExtension}`,
|
||||
`${iconName}${separateIconFileExportExtension ?? iconFileExtension}`
|
||||
);
|
||||
|
||||
await fs.promises.writeFile(location, output, 'utf-8');
|
||||
|
||||
@@ -13,6 +13,8 @@ export type TemplateFunction = (params: {
|
||||
getSvg: () => Promise<string>;
|
||||
deprecated?: boolean;
|
||||
deprecationReason?: string;
|
||||
aliases?: (string | AliasDeprecation)[];
|
||||
toPascalCase: (value: string) => string;
|
||||
}) => Promise<string>;
|
||||
|
||||
export type Path = string;
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface ExportTemplate {
|
||||
getSvg: () => Promise<string>;
|
||||
deprecated: boolean;
|
||||
deprecationReason: string;
|
||||
aliases: Array<string | { name: string }>;
|
||||
toPascalCase: (value: string) => string;
|
||||
}
|
||||
|
||||
export type TemplateFunction = (params: ExportTemplate) => Promise<string>;
|
||||
|
||||
Reference in New Issue
Block a user