Compare commits

..

6 Commits

38 changed files with 2517 additions and 1423 deletions

View File

@@ -95,9 +95,25 @@
}
]
},
"lucide-angular": {
"@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,
"icon": "angular",
"shields": [
{
"alt": "npm",
@@ -112,7 +128,7 @@
]
},
"lucide-preact": {
"order": 7,
"order": 8,
"icon": "preact",
"shields": [
{
@@ -130,7 +146,7 @@
"@lucide/astro": {
"docsAlias": "lucide-astro",
"packageDirname": "astro",
"order": 8,
"order": 9,
"icon": "astro",
"iconDark": "astro-dark",
"shields": [
@@ -147,7 +163,7 @@
]
},
"lucide-static": {
"order": 9,
"order": 10,
"icon": "svg",
"shields": [
{

View File

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

View File

@@ -70,7 +70,7 @@ const value = computed({
color: var(--vp-c-text-2);
padding: 3px 8px 3px 3px;
height: auto;
font-size: 13px;
font-size: 14px;
text-align: left;
border: 1px solid transparent;
cursor: text;
@@ -90,7 +90,7 @@ const value = computed({
border: none;
background: transparent;
color: var(--vp-c-text-1);
font-size: 13px;
font-size: 14px;
text-align: left;
border-radius: 8px;
cursor: text;

View File

@@ -33,7 +33,7 @@ export default {
label: 'Lucide documentation for Preact',
},
{
name: 'lucide-angular',
name: '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/lucide-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/angular), [Astro](https://lucide.dev/guide/packages/lucide-astro), and [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs).
## Community

View File

@@ -0,0 +1,277 @@
# `@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,5 +1,11 @@
# 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

@@ -102,16 +102,10 @@ The example below imports all ES Modules, so exercise caution when using it. Imp
### Icon Component Example
```tsx
import * as icons from 'lucide-react-native/icons';
```jsx
import { icons } from 'lucide-react-native';
interface IconProps {
name: keyof typeof icons;
color?: string;
size?: number;
}
const Icon = ({ name, color, size }: IconProps) => {
const Icon = ({ name, color, size }) => {
const LucideIcon = icons[name];
return <LucideIcon color={color} size={size} />;
@@ -122,11 +116,11 @@ export default Icon;
#### Using the Icon Component
```tsx
```jsx
import Icon from './Icon';
const App = () => {
return <Icon name="House" />;
return <Icon name="house" />;
};
export default App;

View File

@@ -1,37 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"karsa-mistmere"
],
"tags": [
"toolkit",
"tools",
"trunk",
"chest",
"box",
"storage",
"utility",
"utilities",
"container",
"kit",
"set",
"repair",
"fix",
"service",
"maintenance",
"mechanic",
"workshop",
"construction",
"hardware",
"equipment",
"gear",
"handyman",
"engineering",
"craft",
"diy"
],
"categories": [
"tools",
"home"
]
}

View File

@@ -1,17 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 12v4" />
<path d="M16 6a2 2 0 0 1 1.414.586l4 4A2 2 0 0 1 22 12v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 .586-1.414l4-4A2 2 0 0 1 8 6z" />
<path d="M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2" />
<path d="M2 14h20" />
<path d="M8 12v4" />
</svg>

Before

Width:  |  Height:  |  Size: 471 B

View File

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

20
packages/angular/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
// 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"
}
]
}

42
packages/angular/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,42 @@
{
// 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

@@ -0,0 +1,184 @@
# 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,29 +27,33 @@ Lucide icon library for Angular applications.
# Lucide Angular
Implementation of the lucide icon library for angular applications.
A standalone, signal based, zoneless 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/lucide-angular)
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).
## Community

View File

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

View File

@@ -21,7 +21,7 @@
"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:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts --useDefaultExports=0",
"build:ng": "ng build --configuration production",
"test": "ng test --no-watch",
"test:watch": "ng test",
@@ -58,6 +58,7 @@
"@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",
@@ -69,7 +70,7 @@
"vitest": "^4.0.16"
},
"peerDependencies": {
"@angular/common": "13.x - 21.x",
"@angular/core": "13.x - 21.x"
"@angular/common": "17.x - 21.x",
"@angular/core": "17.x - 21.x"
}
}

View File

@@ -1,5 +1,6 @@
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,
@@ -9,7 +10,6 @@ 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 iconName = '${iconName}';
static iconData: LucideIconData = ${JSON.stringify(children)};
override readonly iconName = signal(${angularComponentName}.iconName);
override readonly iconData = signal(${angularComponentName}.iconData);
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);
}
${aliasComponentNames.map((aliasComponentName) => {

View File

@@ -1,18 +1,42 @@
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.
* Lucide icon configuration options.
*/
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,
@@ -20,13 +44,18 @@ export const lucideDefaultConfig: LucideConfig = {
absoluteStrokeWidth: false,
};
export const LUCIDE_CONFIG = new InjectionToken<LucideConfig>(
'Lucide icon config',
{
factory: () => lucideDefaultConfig,
},
);
/**
* Injection token for providing default configuration options.
*
* @internal Use {@link provideLucideConfig}
*/
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,26 +46,61 @@ function transformNumericStringInput(
},
})
export abstract class LucideIconBase {
abstract iconName: Signal<Nullable<string>>;
abstract iconData: Signal<Nullable<LucideIconData>>;
protected abstract readonly iconName: Signal<Nullable<string>>;
protected abstract readonly 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(() => {
protected 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,
});
@@ -83,20 +118,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');
for (const [name, value] of Object.entries(attrs)) {
Object.entries(attrs).forEach(([name, value]) =>
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);
}
elements.forEach((element) =>
this.renderer.removeChild(this.elRef.nativeElement, element),
);
});
}
});

View File

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

View File

@@ -3,10 +3,40 @@ 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,20 +5,27 @@ export type LucideIconNode = readonly [string, HtmlAttributes];
export type LucideIconData = readonly LucideIconNode[];
export type LucideIcons = { [key: string]: LucideIconData };
export interface LucideIconComponentInterface {
/**
* 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<{
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 &&
@@ -31,4 +38,7 @@ export function isLucideIconComponent(icon: unknown): icon is LucideIconComponen
export type LucideIconInput = LucideIconComponentType | LucideIconData | string;
/**
* @internal
*/
export type Nullable<T> = T | null | undefined;

View File

@@ -1,101 +0,0 @@
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

@@ -1,213 +0,0 @@
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;
}
}

View File

@@ -24,23 +24,11 @@
"author": "Eric Fennis",
"amdName": "lucide-react-native",
"main": "dist/cjs/lucide-react-native.js",
"main:umd": "dist/umd/lucide-react-native.js",
"module": "dist/esm/lucide-react-native.js",
"unpkg": "dist/umd/lucide-react-native.min.js",
"typings": "dist/lucide-react-native.d.ts",
"react-native": "dist/esm/lucide-react-native.js",
"exports": {
".": {
"types": "./dist/lucide-react-native.d.ts",
"import": "./dist/esm/lucide-react-native.js",
"browser": "./dist/esm/lucide-react-native.js",
"require": "./dist/cjs/lucide-react-native.js"
},
"./icons": {
"types": "./dist/icons.d.ts",
"import": "./dist/esm/icons/index.js",
"browser": "./dist/esm/icons/index.js",
"require": "./dist/cjs/icons/index.js"
}
},
"sideEffects": false,
"files": [
"dist"

View File

@@ -5,7 +5,7 @@ import pkg from './package.json' with { type: 'json' };
const packageName = 'LucideReact';
const outputFileName = 'lucide-react-native';
const outputDir = 'dist';
const inputs = ['src/lucide-react-native.ts', 'src/icons/index.ts'];
const inputs = ['src/lucide-react-native.ts'];
const bundles = [
{
format: 'cjs',
@@ -60,16 +60,6 @@ export default [
],
plugins: [dts()],
},
{
input: inputs[1],
output: [
{
file: `dist/icons.d.ts`,
format: 'es',
},
],
plugins: [dts()],
},
{
input: `src/${outputFileName}.suffixed.ts`,
output: [

View File

@@ -31,7 +31,6 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
absoluteStrokeWidth,
children,
iconNode,
className,
...rest
},
ref,
@@ -47,7 +46,6 @@ const Icon = forwardRef<SVGSVGElement, IconComponentProps>(
{
ref,
...defaultAttributes,
className,
width: size,
height: size,
...customAttrs,

View File

@@ -1,4 +1,5 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases/prefixed';
export * from './types';

View File

@@ -1,4 +1,5 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases/suffixed';
export * from './types';

View File

@@ -1,4 +1,5 @@
export * from './icons';
export * as icons from './icons';
export * from './aliases';
export * from './types';

2700
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -101,7 +101,6 @@ async function init() {
useCSSVars: false,
outSVGReact: false,
outSVGPath: false,
addLigatures: true,
svgicons2svgfont: {
fontHeight: 1000, // At least 1000 is recommended
normalize: false,

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,4 @@
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>;
import type { TemplateFunction } from '../types.ts';
const defineExportTemplate = (exportFunction: TemplateFunction) => exportFunction;