Compare commits

..

11 Commits

Author SHA1 Message Date
Karsa
c8dda3d4e7 Merge branch 'main' into package/angularv17 2025-12-19 09:08:54 +01:00
Karsa
a83fba9ecb fix(packages/angular): move to packages/angular, update github actions and templates 2025-12-19 09:07:20 +01:00
Karsa
2a9f3fa72e fix(packages/angular): revert changes made to lucide-angular 2025-12-17 21:47:09 +01:00
Karsa
ae1ca07e36 fix(packages/angular-next): added angular-next package keeping original package intact 2025-12-17 21:46:02 +01:00
Karsa
818d99f41e Merge branch 'refs/heads/main' into package/angularv17 2025-12-17 10:48:40 +01:00
Karsa
a3e7e75b90 fix(packages/icons): finalize exportTemplate before migration to input signals & effect to build component data 2025-12-17 09:35:03 +01:00
Karsa
e851a03672 fix(packages/icons): trying some other variations 2025-12-15 11:53:37 +01:00
Karsa
0abfa2f0d5 Merge branch 'refs/heads/main' into package/angularv17
# Conflicts:
#	packages/lucide-angular/package.json
#	packages/lucide-angular/scripts/exportTemplate.mts
#	pnpm-lock.yaml
#	tools/build-icons/building/generateExportsFile.ts
#	tools/build-icons/building/generateIconFiles.ts
2025-12-15 10:05:13 +01:00
Karsa
6c1e34df19 feat(packages): angular v17 dead end 2025-04-19 17:15:08 +02:00
Karsa
669f62bb64 Merge branch 'refs/heads/main' into package/icons 2025-04-19 12:09:52 +02:00
Karsa
708d5114d6 feat(packages): added lucide icons package skeleton 2025-04-01 17:25:10 +02:00
33 changed files with 1349 additions and 2517 deletions

View File

@@ -23,7 +23,7 @@ body:
- label: lucide-solid
- label: lucide-static
- label: lucide-svelte (old version)
- label: '@lucide/svelte (new version)'
- label: `@lucide/svelte (new version)`
- label: lucide-vue
- label: lucide-vue-next
- label: Figma plugin

View File

@@ -23,7 +23,7 @@ body:
- label: lucide-solid
- label: lucide-static
- label: lucide-svelte (old version)
- label: '@lucide/svelte (new version)'
- label: `@lucide/svelte (new version)`
- label: lucide-vue
- label: lucide-vue-next
- label: Figma plugin

View File

@@ -95,24 +95,8 @@
}
]
},
"@lucide/angular": {
"order": 6,
"icon": "angular",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/@lucide/angular",
"href": "https://www.npmjs.com/package/@lucide/angular"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/@lucide/angular",
"href": "https://www.npmjs.com/package/@lucide/angular"
}
]
},
"lucide-angular": {
"order": 7,
"order": 6,
"icon": "angular",
"shields": [
{
@@ -128,7 +112,7 @@
]
},
"lucide-preact": {
"order": 8,
"order": 7,
"icon": "preact",
"shields": [
{
@@ -146,7 +130,7 @@
"@lucide/astro": {
"docsAlias": "lucide-astro",
"packageDirname": "astro",
"order": 9,
"order": 8,
"icon": "astro",
"iconDark": "astro-dark",
"shields": [
@@ -163,7 +147,7 @@
]
},
"lucide-static": {
"order": 10,
"order": 9,
"icon": "svg",
"shields": [
{

View File

@@ -69,39 +69,39 @@ const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
link: '/guide/packages/lucide',
},
{
text: 'React',
text: 'Lucide React',
link: '/guide/packages/lucide-react',
},
{
text: 'Vue',
text: 'Lucide Vue',
link: '/guide/packages/lucide-vue-next',
},
{
text: 'Svelte',
text: 'Lucide Svelte',
link: '/guide/packages/lucide-svelte',
},
{
text: 'Solid',
text: 'Lucide Solid',
link: '/guide/packages/lucide-solid',
},
{
text: 'React Native',
text: 'Lucide React Native',
link: '/guide/packages/lucide-react-native',
},
{
text: 'Angular',
link: '/guide/packages/angular',
text: 'Lucide Angular',
link: '/guide/packages/lucide-angular',
},
{
text: 'Preact',
text: 'Lucide Preact',
link: '/guide/packages/lucide-preact',
},
{
text: 'Astro',
text: 'Lucide Astro',
link: '/guide/packages/lucide-astro',
},
{
text: 'Static',
text: 'Lucide Static',
link: '/guide/packages/lucide-static',
},
],

View File

@@ -33,7 +33,7 @@ export default {
label: 'Lucide documentation for Preact',
},
{
name: 'angular',
name: 'lucide-angular',
logo: '/framework-logos/angular.svg',
label: 'Lucide documentation for Angular',
},

View File

@@ -29,7 +29,7 @@ However, not everyone can understand them easily. Read more about [how to use Lu
## Official Packages
Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](https://lucide.dev/guide/packages/lucide), [React](https://lucide.dev/guide/packages/lucide-react), [React Native](https://lucide.dev/guide/packages/lucide-react-native), [Vue](https://lucide.dev/guide/packages/lucide-vue), [Vue 3](https://lucide.dev/guide/packages/lucide-vue-next), [Svelte](https://lucide.dev/guide/packages/lucide-svelte), [Preact](https://lucide.dev/guide/packages/lucide-preact), [Solid](https://lucide.dev/guide/packages/lucide-solid), [Angular](https://lucide.dev/guide/packages/angular), [Astro](https://lucide.dev/guide/packages/lucide-astro), and [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs).
Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](https://lucide.dev/guide/packages/lucide), [React](https://lucide.dev/guide/packages/lucide-react), [React Native](https://lucide.dev/guide/packages/lucide-react-native), [Vue](https://lucide.dev/guide/packages/lucide-vue), [Vue 3](https://lucide.dev/guide/packages/lucide-vue-next), [Svelte](https://lucide.dev/guide/packages/lucide-svelte), [Preact](https://lucide.dev/guide/packages/lucide-preact), [Solid](https://lucide.dev/guide/packages/lucide-solid), [Angular](https://lucide.dev/guide/packages/lucide-angular), [Astro](https://lucide.dev/guide/packages/lucide-astro), and [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs).
## Community

View File

@@ -1,277 +0,0 @@
# `@lucide/angular`
::: warning
This documentation is for `@lucide/angular`.
To learn about our legacy package for Angular, please refer to [`lucide-angular`](./lucide-angular).
:::
A standalone, signal-based, zoneless implementation of Lucide icons for Angular.
**What you can accomplish:**
- Use icons as standalone Angular components with full dependency injection support
- Configure icons globally through modern Angular providers
- Integrate with Angular's reactive forms and data binding
- Build scalable applications with tree-shaken icons and lazy loading support
## Prerequisites
This package requires Angular 17+ and uses standalone components, signals, and zoneless change detection.
## Installation
::: code-group
```sh [pnpm]
pnpm add @lucide/angular
```
```sh [yarn]
yarn add @lucide/angular
```
```sh [npm]
npm install @lucide/angular
```
```sh [bun]
bun add @lucide/angular
```
:::
## How to use
### Standalone icons
Every icon can be imported as a ready-to-use standalone component:
```html
<svg lucideFileText></svg>
```
```ts{2,7}
import { Component } from '@angular/core';
import { LucideFileText } from '@lucide/angular';
@Component({
selector: 'app-foobar',
templateUrl: './foobar.html',
imports: [LucideFileText],
})
export class Foobar { }
```
::: tip
Standalone icon components use the selector `svg[lucide{PascalCaseIconName}]`.
This ensures minimal bloating of the DOM and the ability to directly manipulate all attributes of the resulting SVG element.
:::
### Dynamic icon component
You may also use the dynamic `LucideIcon` component to dynamically render icons.
#### With tree-shaken imports
You may pass imported icons directly to the component:
```html{3}
@for (item of items) {
<a navbarItem [routerLink]="item.routerLink">
<svg [lucideIcon]="item.icon"></svg>
{{ item.title }}
</a>
}
```
```ts{2,8,14,19}
import { Component } from '@angular/core';
import { LucideIcon, LucideHouse, LucideUsersRound } from '@lucide/angular';
import { NavbarItem, NavbarItemModel } from './navbar-item';
@Component({
selector: 'app-navbar',
templateUrl: './navbar.html',
imports: [LucideIcon, NavbarItem],
})
export class Navbar {
readonly items: NavbarItemModel[] = [
{
title: 'Home',
icon: LucideHouse,
routerLink: [''],
},
{
title: 'Users',
icon: LucideUsersRound,
routerLink: ['admin/users'],
},
];
}
```
#### With icons provided via dependency injection
Alternatively, the component also accepts string inputs.
To use icons this way, first, you have to provide icons via `provideLucideIcons`:
:::code-group
```ts{7-10} [app.config.ts]
import { ApplicationConfig } from '@angular/core';
import { provideLucideIcons, LucideCircleCheck, LucideCircleX } from '@lucide/angular';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideLucideIcons([
LucideCircleCheck,
LucideCircleX,
]),
]
};
```
```html [foobar.html]
<svg lucideIcon="circle-check"></svg>
```
```ts{7} [foobar.ts]
import { Component } from '@angular/core';
import { LucideIcon } from '@lucide/angular';
@Component({
selector: 'app-foobar',
templateUrl: './template-url',
imports: [LucideIcon],
})
export class Foobar { }
```
:::
::: tip
For optimal bundle size, provide icons at the highest appropriate level in your application.
Providing all icons at the root level may increase your initial bundle size, while providing them at feature module level enables better code splitting.
:::
::: warning
While you may provide your icons at any level of the dependency injection tree, be aware that [Angular's DI system is hierarchical](https://angular.dev/guide/di/defining-dependency-providers#injector-hierarchy-in-angular): `LucideIcon` will only have access to the icons provided closest to it in the tree.
:::
## Accessible labels
You can use the `title` input property to set the [accessible name element](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/title) on the SVG:
```html
<svg lucideIcon="house" title="Go to dashboard"></svg>
```
This will result in the following output:
```html{2}
<svg class="lucide lucide-house" ...>
<title>Go to dashboard</title>
<!-- SVG paths -->
</svg>
```
## Props
You can pass additional props to adjust the icon appearance.
| name | type | default |
|-----------------------|-----------|--------------|
| `size` | *number* | 24 |
| `color` | *string* | currentColor |
| `strokeWidth` | *number* | 2 |
| `absoluteStrokeWidth` | *boolean* | false |
```html
<svg lucideHouse size="48" color="red" strokeWidth="1"></svg>
```
## Global configuration
You can use `provideLucideConfig` to configure the default property values as defined above:
```ts{2,7-9}
import { ApplicationConfig } from '@angular/core';
import { provideLucideConfig } from '@lucide/angular';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideLucideConfig({
strokeWidth: 1.5
}),
]
};
```
## Styling via CSS
Icons can also be styled by using custom CSS classes:
```html
<svg lucideHousePlus class="my-icon"></svg>
```
```css
svg.my-icon {
width: 12px;
height: 12px;
stroke-width: 3;
}
```
## With Lucide lab or custom icons
[Lucide lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
While they aren't provided as standalone components, they can be still be passed to the `LucideIcon` component the same way as official icons:
```html
<!-- Directly as LucideIconData: -->
<svg [lucideIcon]="CoconutIcon"></svg>
<!-- As a provided icon by name: -->
<svg lucideIcon="coconut"></svg>
```
```ts{2,6-7,11-12}
import { provideLucideIcons } from '@lucide/angular';
import { coconut } from '@lucide/lab';
@Component({
templateUrl: './foobar.html',
// For using by name via provider:
providers: [provideLucideIcons({ coconut })],
imports: [LucideIcon]
})
export class Foobar {
// For passing directly as LucideIconData:
readonly CoconutIcon = coconut;
}
```
## Troubleshooting
### The icon is not being displayed
If using per-icon-components:
1. Ensure that the icon component is being imported, if using per-icon-components
2. Check that the icon name matches exactly (case-sensitive)
If using the dynamic component:
1. Ensure the icon is provided via `provideLucideIcons()` if using string names
2. Verify the icon is imported from `@lucide/angular` and not the legacy package
### TypeScript errors?
Make sure you're importing from `@lucide/angular` and not `lucide-angular`.
### Icons render with wrong defaults
Ensure `provideLucideConfig()` is used at the right level.
## Migration guide
Migrating from `lucide-angular`? Read our [comprehensive migration guide](https://github.com/lucide-icons/lucide/blob/main/packages/angular/MIGRATION.md).

View File

@@ -1,11 +1,5 @@
# Lucide Angular
::: warning
This documentation if for our legacy package for Angular.
For our modern, standalone-first implementation, please refer to [`@lucide/angular`](./angular).
:::
Angular components and services for Lucide icons that integrate with Angular's dependency injection and component system. Provides both traditional module-based and modern standalone component approaches for maximum flexibility in Angular applications.
**What you can accomplish:**

View File

@@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View File

@@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View File

@@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@@ -1,184 +0,0 @@
# Migrating from `lucide-angular` ⇒ `@lucide/angular`
## What changed
`@lucide/angular` moves from a module + single component based API to a more modern Angular approach:
- The library defines modern signal-based, standalone components, without zone.js based change detection.
- Icons are consumed as standalone imports (one component per icon).
- Dynamic icon registration is done via `provideLucideIcon()`, not using `NgModule`.
- Static icons use per-icon components for better tree-shaking.
- Dynamic icons still use a single dynamic component (`svg[lucideIcon]`).
- Global defaults are configured via `provideLucideConfig()`.
---
## Step 1 Update dependencies
Remove `lucide-angular`, add `@lucide/angular`, see http://lucide.dev/guide/packages/angular#installation
---
## Step 2 Replace `LucideAngularModule.pick(...)` with `provideLucideIcons(...)`
> Notes:
> - Old imports like `AirVentIcon` / `AlarmClock` from `lucide-angular` should be replaced with the new per-icon exports `LucideAirVent` and `LucideAlarmClock`.
> - If you mostly used static icons, you may not need to provide them **at all**, please refer to Step 3.
### Before
#### NgModule based
```ts
import { LucideAngularModule, AirVent, AlarmClock } from 'lucide-angular';
@NgModule({
imports: [
BrowserModule,
LucideAngularModule.pick({ AirVent, AlarmClock }),
],
})
export class AppModule {}
```
#### Standalone
```ts
import { ApplicationConfig } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(LucideAngularModule.pick({ AirVent, AlarmClock })),
]
};
```
### After
```ts
import { ApplicationConfig } from '@angular/core';
import { provideLucideIcons, LucideAirVent, LucideAlarmClock } from '@lucide/angular';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideLucideIcons([
LucideAirVent,
LucideAlarmClock,
]),
]
};
```
---
## Step 3 Replace `<lucide-angular>` / `<lucide-icon>` / `<i-lucide>` / `<span-lucide>`
The legacy package rendered everything through a single component. All of these selectors must be migrated to `<svg>` usage.
### A. Static icons by name
If the icon is known at build time, just use a static import:
#### Before
```html
<lucide-angular name="circle-check"></lucide-angular>
```
#### After
```html
<svg lucideCircleCheck></svg>
```
### B. Static icons with icon data binding
#### Before
```ts
import { CircleCheck } from 'lucide-angular';
```
```html
<lucide-icon [img]="CircleCheck"></lucide-icon>
```
#### After
```ts
import { LucideCircleCheck } from '@lucide/angular';
```
```html
<svg lucideCircleCheck></svg>
```
...and import `LucideCircleCheck` from `@lucide/angular`.
---
### C. Dynamic icons
If the icon varies at runtime, use the dynamic component:
#### Before
```html
<lucide-icon [name]="item.icon"></lucide-icon>
```
#### After
```html
<svg [lucideIcon]="item.icon"></svg>
```
---
## Step 4 Replace `LucideIconConfig` with `provideLucideConfig()`
### Before
```ts
import { inject } from '@angular/core';
import { LucideIconConfig } from 'lucide-angular';
inject(LucideIconConfig).size = 12;
```
### After
```ts
import { provideLucideConfig } from '@lucide/angular';
providers: [
provideLucideConfig({ size: 12 }),
]
```
### Where to place it
- App-wide: `AppModule.providers` or `bootstrapApplication(...providers)`
- Feature-level: feature module providers
- Component-level (standalone): component `providers`
---
## Troubleshooting
### The icon is not being displayed
If using per-icon-components:
1. Ensure that the icon component is being imported, if using per-icon-components
2. Check that the icon name matches exactly (case-sensitive)
If using the dynamic component:
1. Ensure the icon is provided via `provideLucideIcons()` if using string names
2. Verify the icon is imported from `@lucide/angular` and not the legacy package
### TypeScript errors?
Make sure you're importing from `@lucide/angular` and not `lucide-angular`.
### Icons render with wrong defaults
Ensure `provideLucideConfig()` is used at the right level.
---
## TL;DR
- `LucideAngularModule` ⇒ static: removed; dynamic: `LucideIcon`
- `LucideAngularModule.pick(...)``provideLucideIcons(...)`
- `<lucide-angular name="foo-bar">``<svg lucideFooBar>`
- `<lucide-icon [name]="expr">``<svg [lucideIcon]="expr">`
- `<lucide-icon [img]="expr">``<svg [lucideIcon]="expr">`
- `LucideIconConfig``provideLucideConfig(...)`

View File

@@ -27,33 +27,29 @@ Lucide icon library for Angular applications.
# Lucide Angular
A standalone, signal based, zoneless implementation of the Lucide icon library for Angular applications.
Implementation of the lucide icon library for angular applications.
## Installation
```sh
pnpm add @lucide/angular
pnpm add lucide-angular
```
```sh
npm install @lucide/angular
npm install lucide-angular
```
```sh
yarn add @lucide/angular
yarn add lucide-angular
```
```sh
bun add @lucide/angular
bun add lucide-angular
```
## Documentation
For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/angular)
## Migration guide
Migrating from `lucide-angular`? Read our [comprehensive migration guide](./MIGRATION.md).
For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/lucide-angular)
## Community

View File

@@ -1,10 +1,7 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm"
},
"newProjectRoot": ".",
"newProjectRoot": "projects",
"projects": {
"@lucide/angular": {
"projectType": "library",
@@ -47,5 +44,8 @@
}
}
}
},
"cli": {
"packageManager": "pnpm"
}
}

View File

@@ -18,12 +18,12 @@
"ng": "ng",
"watch": "ng build --watch --configuration development",
"prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons",
"build": "pnpm prebuild && pnpm build:ng",
"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 --useDefaultExports=0",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts",
"build:ng": "ng build --configuration production",
"test": "pnpm prebuild && ng test --no-watch",
"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",
@@ -58,7 +58,6 @@
"@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0",
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "workspace:*",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "^4.0.16",
"angular-eslint": "21.1.0",
@@ -70,7 +69,7 @@
"vitest": "^4.0.16"
},
"peerDependencies": {
"@angular/common": "17.x - 21.x",
"@angular/core": "17.x - 21.x"
"@angular/common": "13.x - 21.x",
"@angular/core": "13.x - 21.x"
}
}

View File

@@ -1,6 +1,5 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
import { toPascalCase } from '@lucide/helpers';
export default defineExportTemplate(async ({
componentName,
@@ -10,6 +9,7 @@ export default defineExportTemplate(async ({
deprecated,
deprecationReason,
aliases = [],
toPascalCase,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
@@ -49,10 +49,10 @@ import { Component, signal } from '@angular/core';
standalone: true,
})
export class ${angularComponentName} extends LucideIconBase {
static readonly iconName = '${iconName}';
static readonly iconData: LucideIconData = ${JSON.stringify(children)};
protected override readonly iconName = signal(${angularComponentName}.iconName);
protected override readonly iconData = signal(${angularComponentName}.iconData);
static iconName = '${iconName}';
static iconData: LucideIconData = ${JSON.stringify(children)};
override readonly iconName = signal(${angularComponentName}.iconName);
override readonly iconData = signal(${angularComponentName}.iconData);
}
${aliasComponentNames.map((aliasComponentName) => {

View File

@@ -1,42 +1,18 @@
import { InjectionToken, Provider } from '@angular/core';
/**
* Lucide icon configuration options.
* 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 {
/**
* Stroke color.
* @default currentColor
*/
color: string;
/**
* Width and height.
* @default 24
*/
size: number;
/**
* Stroke width
* @default 2
*/
strokeWidth: number;
/**
* Whether stroke width should be scaled to appear uniform regardless of icon size.
* @default false
*
* @remarks
* Use CSS to set on SVG paths instead:
* ```css
* .lucide * {
* vector-effect: non-scaling-stroke;
* }`
* ```
*/
absoluteStrokeWidth: boolean;
}
/**
* Default icon configuration options.
*/
export const lucideDefaultConfig: LucideConfig = {
color: 'currentColor',
size: 24,
@@ -44,18 +20,13 @@ export const lucideDefaultConfig: LucideConfig = {
absoluteStrokeWidth: false,
};
/**
* Injection token for providing default configuration options.
*
* @internal Use {@link provideLucideConfig}
*/
export const LUCIDE_CONFIG = new InjectionToken<LucideConfig>('Lucide icon config', {
export const LUCIDE_CONFIG = new InjectionToken<LucideConfig>(
'Lucide icon config',
{
factory: () => lucideDefaultConfig,
});
},
);
/**
* Provider for default icon configuration options.
*/
export function provideLucideConfig(config: Partial<LucideConfig>): Provider {
return {
provide: LUCIDE_CONFIG,

View File

@@ -46,61 +46,26 @@ function transformNumericStringInput(
},
})
export abstract class LucideIconBase {
protected abstract readonly iconName: Signal<Nullable<string>>;
protected abstract readonly iconData: Signal<Nullable<LucideIconData>>;
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);
protected readonly ariaHidden = computed(() => {
readonly title = input<Nullable<string>>();
readonly ariaHidden = computed(() => {
return !this.title();
});
/**
* An optional accessible label for the icon.
* - If provided, it will add the title as an [`<svg:title>` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/title).
* - If not provided, the component will add an `aria-hidden="true"` attribute automatically.
*
* @remarks
* Please refer to our [Accessibility guide](https://lucide.dev/guide/advanced/accessibility) regarding this matter.
* Adding accessible labels to icons is normally not necessary:
* - If your icon is decorative (as most icons are) just leave it as hidden from screen readers.
* - If your icon is interactive, it should be contained within an interactive element (e.g. button), and you should probably set your accessible label on that element.
* - If your icon is functional (e.g. used in place of a label), feel free to use this property.
*/
readonly title = input<Nullable<string>>();
/**
* Width and height.
* @default 24
*/
readonly size = input(this.iconConfig.size, {
transform: (value: Nullable<string | number>) =>
transformNumericStringInput(value, this.iconConfig.size),
});
/**
* Stroke color.
* @default currentColor
*/
readonly color = input(this.iconConfig.color, {
transform: (value: Nullable<string>) => value ?? this.iconConfig.color,
});
/**
* Stroke width
* @default 2
*/
readonly strokeWidth = input(this.iconConfig.strokeWidth, {
transform: (value: Nullable<string | number>) =>
transformNumericStringInput(value, this.iconConfig.strokeWidth),
});
/**
* Whether stroke width should be scaled to appear uniform regardless of icon size.
*
* @remarks
* Use CSS to set on SVG paths instead:
* ```css
* .lucide * {
* vector-effect: non-scaling-stroke;
* }
* ```
*/
readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, {
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.absoluteStrokeWidth,
});
@@ -118,20 +83,20 @@ export abstract class LucideIconBase {
if (icon) {
const elements = icon.map(([name, attrs]) => {
const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg');
Object.entries(attrs).forEach(([name, value]) =>
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(() => {
elements.forEach((element) =>
this.renderer.removeChild(this.elRef.nativeElement, element),
);
for (const element of elements) {
this.renderer.removeChild(this.elRef.nativeElement, element);
}
});
}
});

View File

@@ -85,8 +85,8 @@ describe('LucideIcon', () => {
name.set('custom-name');
fixture.detectChanges();
expect(component['iconData']()).toBe(testIcon);
expect(component['iconName']()).toBe('custom-name');
expect(component.iconData()).toBe(testIcon);
expect(component.iconName()).toBe('custom-name');
expect(fixture.nativeElement.innerHTML).toBe(
'<!--container--><polyline points="1 1 22 22"></polyline>',
);
@@ -95,15 +95,15 @@ describe('LucideIcon', () => {
icon.set(LucideActivity);
fixture.detectChanges();
expect(component['iconData']()).toBe(LucideActivity.iconData);
expect(component['iconName']()).toBe(LucideActivity.iconName);
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');
expect(component.iconData()).toBe(testIcon);
expect(component.iconName()).toBe('demo');
});
it('should throw error if no icon founds', () => {
icon.set('invalid');

View File

@@ -10,9 +10,6 @@ interface LucideResolvedIcon {
data: LucideIconData;
}
/**
* Generic icon component for rendering LucideIconData.
*/
@Component({
selector: 'svg[lucideIcon]',
templateUrl: './lucide-icon.html',
@@ -27,10 +24,10 @@ export class LucideIcon extends LucideIconBase {
readonly resolvedIcon = computed<LucideResolvedIcon | null>(() => {
return this.resolveIcon(this.name(), this.iconInput());
});
protected override readonly iconName = computed(() => {
override readonly iconName = computed(() => {
return this.resolvedIcon()?.name;
});
protected override readonly iconData = computed(() => {
override readonly iconData = computed(() => {
return this.resolvedIcon()?.data;
});

View File

@@ -3,40 +3,10 @@ import { LucideIconData, LucideIcons } from './types';
import { isLucideIconComponent, LucideIconComponentType } from './types';
import { toKebabCase } from './utils/to-kebab-case';
/**
* Injection token for providing Lucide icons by name.
*
* @internal Use {@link provideLucideConfig}
*/
export const LUCIDE_ICONS = new InjectionToken<LucideIcons>('Lucide icons', {
factory: () => ({}),
});
/**
* Provide Lucide icons by name.
*
* @remarks
* Warning! This provider will convert dictionary keys to lower-kebab-case.
*
* @param icons Either a dictionary of icons or a list of Angular icon components.
*
* @usage
* ```ts
* import { provideLucideIcons, SquareCheck } from '@lucide/angular';
* import { MyCustomIcon } from './custom-icons/circle-check';
*
* providers: [
* provideLucideIcons({
* SquareCheck,
* MyCustomIcon, // LucideIconData
* }),
* ]
* ```
*
* ```html
* <svg lucideIcon="my-custom-icon" />
* ```
*/
export function provideLucideIcons(
icons: Record<string, LucideIconData | LucideIconComponentType> | Array<LucideIconComponentType>,
): Provider {

View File

@@ -5,30 +5,20 @@ export type LucideIconNode = readonly [string, HtmlAttributes];
export type LucideIconData = readonly LucideIconNode[];
export type LucideIcons = { [key: string]: LucideIconData };
/**
* Represents a Lucide icon component that has `iconName` and `iconData` signals inherited from `LucideIconBase` and respective static members accessible without instantiating the component.
*/
export type LucideIconComponentType = Type<{
title: Signal<Nullable<string>>;
size: Signal<Nullable<number>>;
color: Signal<Nullable<string>>;
strokeWidth: Signal<Nullable<number>>;
absoluteStrokeWidth: Signal<Nullable<boolean>>;
}> & {
export interface LucideIconComponentInterface {
iconName: Signal<Nullable<string>>;
iconData: Signal<Nullable<LucideIconData>>;
}
export type LucideIconComponentType = Type<LucideIconComponentInterface> & {
iconName: string;
iconData: LucideIconData;
};
/**
* Type guard for {@link LucideIconData}
*/
export function isLucideIconData(icon: unknown): icon is LucideIconData {
return Array.isArray(icon);
}
/**
* Type guard for {@link LucideIconComponentType}
*/
export function isLucideIconComponent(icon: unknown): icon is LucideIconComponentType {
return (
icon instanceof Type &&
@@ -41,7 +31,4 @@ export function isLucideIconComponent(icon: unknown): icon is LucideIconComponen
export type LucideIconInput = LucideIconComponentType | LucideIconData | string;
/**
* @internal
*/
export type Nullable<T> = T | null | undefined;

View File

@@ -4,7 +4,9 @@
"compileOnSave": false,
"compilerOptions": {
"paths": {
"@lucide/angular": ["./dist"],
"@lucide/angular": [
"./dist"
]
},
"strict": true,
"noImplicitOverride": true,
@@ -18,21 +20,21 @@
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve",
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.lib.json",
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json",
},
],
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -9,6 +9,10 @@
"inlineSources": true,
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["**/*.spec.ts"]
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.spec.ts"
]
}

View File

@@ -4,7 +4,12 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["vitest/globals"]
"types": [
"vitest/globals"
]
},
"include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
"include": [
"src/**/*.d.ts",
"src/**/*.spec.ts"
]
}

View 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;
}
}
});

View 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;
}
}

2672
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,6 @@ export default async function generateExportFile(
iconNodes: Record<string, INode>,
exportModuleNameCasing: 'camel' | 'pascal',
iconFileExtension = '',
useDefaultExports = true,
) {
const fileName = path.basename(inputEntry);
@@ -26,9 +25,7 @@ export default async function generateExportFile(
} else if (exportModuleNameCasing === 'pascal') {
componentName = toPascalCase(iconName);
}
const importString = `export ${
useDefaultExports ? `{ default as ${componentName} }` : `*`
} from './${iconName}${iconFileExtension}';\n`;
const importString = `export * from './${iconName}${iconFileExtension}';\n`;
return appendFile(importString, fileName, outputDirectory);
});

View File

@@ -51,7 +51,7 @@ function generateIconFiles({
const {
deprecated = false,
toBeRemovedInVersion = undefined,
aliases = [],
aliases,
} = iconMetaData[iconName];
const deprecationReason = deprecated
? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', {
@@ -69,6 +69,7 @@ function generateIconFiles({
deprecated,
deprecationReason,
aliases,
toPascalCase,
});
const output = pretty
@@ -86,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');

View File

@@ -31,7 +31,6 @@ interface CliArguments {
separateIconFileExportExtension?: string;
aliasesFileExtension?: string;
aliasImportFileExtension?: string;
useDefaultExports?: boolean;
pretty?: boolean;
output: string | undefined;
}
@@ -63,7 +62,6 @@ const {
separateIconFileExportExtension = undefined,
aliasesFileExtension = '.js',
aliasImportFileExtension = '',
useDefaultExports = true,
pretty = true,
} = cliArguments;
@@ -127,7 +125,6 @@ async function buildIcons() {
icons,
exportModuleNameCasing,
importImportFileExtension,
useDefaultExports,
);
}

View File

@@ -6,17 +6,16 @@ export type IconNode = [tag: string, attrs: SVGProps][];
export type IconNodeWithChildren = [tag: string, attrs: SVGProps, children: IconNode];
export interface ExportTemplate {
export type TemplateFunction = (params: {
componentName: string;
iconName: string;
children: IconNode;
getSvg: () => Promise<string>;
deprecated: boolean;
deprecationReason: string;
deprecated?: boolean;
deprecationReason?: string;
aliases?: (string | AliasDeprecation)[];
}
export type TemplateFunction = (params: ExportTemplate) => Promise<string>;
toPascalCase: (value: string) => string;
}) => Promise<string>;
export type Path = string;

View File

@@ -1,4 +1,17 @@
import type { TemplateFunction } from '../types.ts';
import { type IconNode } from '../types.ts';
export interface ExportTemplate {
componentName: string;
iconName: string;
children: IconNode;
getSvg: () => Promise<string>;
deprecated: boolean;
deprecationReason: string;
aliases: Array<string | { name: string }>;
toPascalCase: (value: string) => string;
}
export type TemplateFunction = (params: ExportTemplate) => Promise<string>;
const defineExportTemplate = (exportFunction: TemplateFunction) => exportFunction;