Compare commits

..

15 Commits

Author SHA1 Message Date
Karsa
2bb9c9e059 test(packages/icons): update absoluteStrokeWidth unit test 2025-12-22 10:55:06 +01:00
Karsa
3b0c39cf13 fix(packages/icons): update absoluteStrokeWidth implementation 2025-12-22 10:51:37 +01:00
Karsa
76861c621f fix(packages/icons): update readme and documentation 2025-12-22 09:37:15 +01:00
Eric Fennis
4869c6409a Merge branch 'next' into package/icons 2025-12-22 08:14:13 +01:00
Karsa
eae3dc5f94 fix(pnpm): update lockfile 2025-12-19 14:56:52 +01:00
Karsa
00954a6203 Merge branch 'main' into package/icons 2025-12-19 14:56:20 +01:00
Karsa
d152818621 feat(packages/icons): rename from packages/lucide-icons => packages/icons 2025-12-19 10:00:45 +01:00
Karsa
abff584694 fix(packages/icons): fix linting issues in configs 2025-12-12 18:24:46 +01:00
Karsa
5d8110882d fix(.github/ISSUE_TEMPLATE): fix linting issue (@ is a reserved character) 2025-12-12 18:23:26 +01:00
Karsa
2ea256381c docs(packages/icons): add documentation for various helpers and types 2025-12-12 18:20:52 +01:00
Karsa
b88bcae614 fix(packages/icons): update github actions 2025-12-12 18:20:52 +01:00
Karsa
1bbcaf8c1c fix(packages/icons): update package, remove UMD export 2025-12-12 18:20:52 +01:00
Karsa
68ea9b2736 fix: fixed linting issues introduced in c4e5730bc4 (#3858) 2025-12-12 18:20:52 +01:00
Karsa
620b478a2e feat(packages): added initial commit of @lucide/icons 2025-12-12 18:19:31 +01:00
Karsa
6c3bd53c35 feat(packages): added lucide icons package skeleton 2025-12-12 18:19:31 +01:00
88 changed files with 1401 additions and 5820 deletions

View File

@@ -13,19 +13,17 @@ body:
description: Which Lucide packages are affected? You may select more than one. description: Which Lucide packages are affected? You may select more than one.
options: options:
- label: lucide - label: lucide
- label: lucide-angular (old version) - label: lucide-angular
- label: '@lucide/angular (new version)'
- label: '@lucide/astro'
- label: lucide-flutter - label: lucide-flutter
- label: lucide-preact - label: lucide-preact
- label: lucide-react - label: lucide-react
- label: lucide-react-native - label: lucide-react-native
- label: lucide-solid - label: lucide-solid
- label: lucide-static - label: lucide-svelte
- label: lucide-svelte (old version)
- label: '@lucide/svelte (new version)'
- label: lucide-vue - label: lucide-vue
- label: lucide-vue-next - label: lucide-vue-next
- label: lucide-astro
- label: '@lucide/icons'
- label: Figma plugin - label: Figma plugin
- label: source/main - label: source/main
- label: other/not relevant - label: other/not relevant

View File

@@ -13,23 +13,20 @@ body:
description: Which Lucide project do you wish this feature were added to? You may select more than one. description: Which Lucide project do you wish this feature were added to? You may select more than one.
options: options:
- label: lucide - label: lucide
- label: lucide-angular (old version) - label: lucide-angular
- label: '@lucide/angular (new version)'
- label: '@lucide/astro'
- label: lucide-flutter - label: lucide-flutter
- label: lucide-preact - label: lucide-preact
- label: lucide-react - label: lucide-react
- label: lucide-react-native - label: lucide-react-native
- label: lucide-solid - label: lucide-solid
- label: lucide-static - label: lucide-svelte
- label: lucide-svelte (old version)
- label: '@lucide/svelte (new version)'
- label: lucide-vue - label: lucide-vue
- label: lucide-vue-next - label: lucide-vue-next
- label: lucide-astro
- label: '@lucide/icons'
- label: Figma plugin - label: Figma plugin
- label: all JS packages - label: all JS packages
- label: site - label: site
- label: other/not relevant
validations: validations:
required: true required: true
- type: textarea - type: textarea

7
.github/labeler.yml vendored
View File

@@ -59,7 +59,6 @@
🅰️ angular package: 🅰️ angular package:
- changed-files: - changed-files:
- any-glob-to-any-file: - any-glob-to-any-file:
- 'packages/angular/*'
- 'packages/lucide-angular/*' - 'packages/lucide-angular/*'
# For changes in the lucide preact package # For changes in the lucide preact package
@@ -86,6 +85,12 @@
- any-glob-to-any-file: - any-glob-to-any-file:
- 'packages/astro/*' - 'packages/astro/*'
# For changes in the @lucide/icons package
❇️ lucide-icons:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-icons/*'
# For changes in the lucide static package # For changes in the lucide static package
🪨 static package: 🪨 static package:
- changed-files: - changed-files:

View File

@@ -1,10 +1,12 @@
name: Lucide Angular checks name: Lucide Icons Checks
on: on:
pull_request: pull_request:
paths: paths:
- packages/angular/** - packages/icons/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
jobs: jobs:
@@ -22,7 +24,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build - name: Build
run: pnpm --filter @lucide/angular build run: pnpm --filter @lucide/icons build
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -38,4 +40,4 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Test - name: Test
run: pnpm --filter @lucide/angular test run: pnpm --filter @lucide/icons test

View File

@@ -58,9 +58,9 @@ jobs:
'lucide-preact', 'lucide-preact',
'lucide-solid', 'lucide-solid',
'lucide-svelte', 'lucide-svelte',
'@lucide/angular',
'@lucide/astro', '@lucide/astro',
'@lucide/svelte', '@lucide/svelte',
'@lucide/icons',
'@lucide/vue', '@lucide/vue',
] ]
steps: steps:

View File

@@ -87,24 +87,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": { "lucide-angular": {
"order": 7, "order": 6,
"icon": "angular", "icon": "angular",
"shields": [ "shields": [
{ {
@@ -120,7 +104,7 @@
] ]
}, },
"lucide-preact": { "lucide-preact": {
"order": 8, "order": 7,
"icon": "preact", "icon": "preact",
"shields": [ "shields": [
{ {
@@ -138,7 +122,7 @@
"@lucide/astro": { "@lucide/astro": {
"docsAlias": "lucide-astro", "docsAlias": "lucide-astro",
"packageDirname": "astro", "packageDirname": "astro",
"order": 9, "order": 8,
"icon": "astro", "icon": "astro",
"iconDark": "astro-dark", "iconDark": "astro-dark",
"shields": [ "shields": [
@@ -155,7 +139,7 @@
] ]
}, },
"lucide-static": { "lucide-static": {
"order": 10, "order": 9,
"icon": "svg", "icon": "svg",
"shields": [ "shields": [
{ {
@@ -169,5 +153,23 @@
"href": "https://www.npmjs.com/package/lucide-static" "href": "https://www.npmjs.com/package/lucide-static"
} }
] ]
},
"@lucide/icons": {
"docsAlias": "icons",
"packageDirname": "icons",
"order": 10,
"icon": "ts",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/@lucide/icons",
"href": "https://www.npmjs.com/package/@lucide/icons"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/@lucide/icons",
"href": "https://www.npmjs.com/package/@lucide/icons"
}
]
} }
} }

View File

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

View File

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

View File

@@ -79,6 +79,16 @@ declare module "lucide-react-native" {
} }
``` ```
```ts [@lucide-icons]
declare module "@lucide/icons" {
// Prefixed import names
export * from "@lucide/icons/dist/lucide-icons.prefixed";
// or
// Suffixed import names
export * from "@lucide/icons/dist/lucide-icons.suffixed";
}
```
::: :::
Place this in your project root or in a folder where your tsconfig.json is located, or locate it in your defined type directory. Place this in your project root or in a folder where your tsconfig.json is located, or locate it in your defined type directory.

View File

@@ -29,7 +29,7 @@ However, not everyone can understand them easily. Read more about [how to use Lu
## Official Packages ## 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 ## 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

@@ -0,0 +1,172 @@
# @lucide/icons
`@lucide/icons` is a helper library that exports Lucide **icon data** in a tree-shakable format, also providing utilities for dynamic importing icons.
It intentionally ships **no real rendering logic or components** — other packages (for example [`@lucide/angular`](http://npmjs.com/package/@lucide/angular)) can consume this data to render icons in their respective
frameworks. You can also use this package to build third-party integrations for frameworks we don't (yet) support.
## Installation
::: code-group
```sh [pnpm]
pnpm install @lucide/icons
```
```sh [yarn]
yarn add @lucide/icons
```
```sh [npm]
npm install @lucide/icons
```
```sh [bun]
bun add @lucide/icons
```
:::
## Icon data format
Each icon is described by the following interface:
```typescript
export type LucideIconData = {
name: string;
node: LucideIconNode[];
} & (
| { size: number }
| { width: number; height: number; }
);
```
| name | type | description |
|------------------------------|--------------------|--------------------------------------------------------------------|
| `name` | `string` | The name of the icon. |
| `node` | `LucideIconNode[]` | SVG child nodes as `[tagName, attributes]` tuples. |
| `size` or `width` & `height` | `number` | The dimensions of the icon (`size` is shorthand for square icons). |
## How to use
Icons can be imported individually. Only the icons you import end up referenced by your application code — the rest will be eliminated by tree-shaking.
```ts
import { House } from '@lucide/icons';
// House is icon data (not a rendered component).
```
## Building icons
`@lucide/icons` ships small helpers that convert Lucide icon data into different render-ready outputs.
All builders accept the same `params` object (`LucideBuildParams`) to customize the generated SVG.
### Build parameters
The following parameters are supported (names reflect the current implementation):
| param | type | effect |
|-----------------------|--------------------------|------------------------------------------------------------------------------------|
| `color` | `string` | Sets `stroke` (defaults to `currentColor`). |
| `size` | `number` | Sets both `width` and `height` (defaults to 24). |
| `width` | `number` | Sets `width` only. |
| `height` | `number` | Sets `height` only. |
| `strokeWidth` | `number` | Sets `stroke-width` (defaults to 2). |
| `absoluteStrokeWidth` | `boolean` | Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. |
| `className` | `string` | Appended to the generated `class` attribute. |
| `attributes` | `Record<string, string>` | Add or override any generated SVG attributes (including `class`, `viewBox`, etc.). |
::: info
SVG attributes generated by the builders include a default Lucide setup (`xmlns`, `viewBox`, `fill="none"`, `stroke="currentColor"`, `stroke-width="2"`, `stroke-linecap="round"`, `stroke-linejoin="round"`), plus a class string of the form: `lucide lucide-{iconName}`.
:::
### `buildLucideIconNode`
Creates a root SVG node in an svgson-like format:
```ts
import { buildLucideIconNode } from '@lucide/icons/builders';
import { House } from '@lucide/icons';
const node = buildLucideIconNode(House, {
size: 32,
strokeWidth: 1.5,
className: 'my-icon',
});
// -> ['svg', attributes, children]
```
This is useful if you want to plug Lucide icons into your own renderer, templating system, or framework integration.
### `buildLucideSvg`
Creates an SVG string:
```ts
import { buildLucideSvg } from '@lucide/icons/builders';
import { House } from '@lucide/icons';
const svg = buildLucideSvg(House, { size: 24, color: '#111' });
```
### `buildLucideIconElement`
Creates an actual DOM element (SVG) within the provided document:
```ts
import { buildLucideIconElement } from '@lucide/icons/builders';
import { House } from '@lucide/icons';
const el = buildLucideIconElement(document, House, { size: 24 });
document.body.appendChild(el);
```
### `buildLucideDataUri`
Creates a base64-encoded SVG data URI from a Lucide icon object.
This helper works in both browsers and Node.js:
- In browsers it uses `btoa` (with proper UTF-8 handling)
- In Node.js it falls back to `Buffer`
```ts
import { buildLucideDataUri } from '@lucide/icons/builders';
import { House } from '@lucide/icons';
const uri = buildLucideDataUri(House, { size: 24 });
```
The returned value can be used directly in places such as:
- `<img src="...">`
- CSS `background-image`
- Canvas drawing
- Inline data URLs in HTML or SVG
::: tip Environment notes
- The SVG is encoded as UTF-8 before base64 conversion to ensure correct handling of non-ASCII characters.
- No runtime configuration is required — the function automatically selects the appropriate encoding strategy.
- If neither `btoa` nor `Buffer` is available, an error is thrown.
:::
## Dynamic imports
Dynamic imports are useful when you only know the icon name at runtime (for example, icon names stored in a database or a CMS). For purely static use cases, prefer direct imports for the best tree-shaking results.
::: tip
Validate `iconName` before indexing the map (and provide a fallback icon) to avoid runtime errors.
:::
## Dynamic imports
Dynamic imports are useful when the icon name is only known at runtime (for example, icon names stored in a CMS or database). For purely static usage, prefer direct imports for maximum tree-shaking.
```ts
import { lucideDynamicIconImports } from '@lucide/icons/dynamic';
const name = 'house';
const icon = await lucideDynamicIconImports[name]?.();
if (!icon) {
// handle unknown icon name (fallback)
}
```

View File

@@ -1,11 +1,5 @@
# Lucide Angular # Lucide Angular
::: warning
This documentation is 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. 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:** **What you can accomplish:**

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="#3178C6" d="M24 0H0v24h24V0Z"/>
<path fill="#fff" fill-rule="evenodd" d="M14.11 18.797v2.588c.421.215.918.375 1.49.487a9.45 9.45 0 0 0 1.819.16 8.35 8.35 0 0 0 1.772-.179 4.407 4.407 0 0 0 1.472-.59c.421-.272.75-.628 1.003-1.07.243-.44.365-.983.365-1.63 0-.47-.065-.882-.206-1.238a2.98 2.98 0 0 0-.61-.947 4.48 4.48 0 0 0-.946-.74 11.05 11.05 0 0 0-1.247-.62c-.338-.14-.647-.27-.919-.412a4.124 4.124 0 0 1-.684-.403 1.784 1.784 0 0 1-.44-.44.928.928 0 0 1-.15-.525c0-.178.046-.338.14-.478.093-.141.225-.263.384-.366.169-.103.375-.178.619-.234.244-.057.515-.085.806-.085.216 0 .44.019.684.047.244.028.479.084.722.15.244.066.479.15.713.253.225.103.44.225.637.356v-2.418a6.524 6.524 0 0 0-1.293-.338 10.228 10.228 0 0 0-1.604-.112c-.618 0-1.2.065-1.753.197a4.42 4.42 0 0 0-1.453.618c-.422.282-.75.638-.993 1.07-.244.43-.366.955-.366 1.555 0 .77.225 1.425.666 1.978.45.544 1.125 1.004 2.024 1.388.357.15.685.29.994.431.31.14.572.281.797.44.225.16.403.32.534.498.132.178.197.384.197.61a.921.921 0 0 1-.121.468c-.085.14-.207.262-.366.375-.16.112-.366.187-.619.244a3.957 3.957 0 0 1-.862.084 4.938 4.938 0 0 1-1.67-.3 4.752 4.752 0 0 1-1.537-.872Zm-4.313-6.45h3.328V10.22H3.844v2.128h3.31v9.497h2.633v-9.497h.01Z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="32" fill="none" viewBox="0 0 128 32">
<rect width="128" height="32" rx="8" fill="#fff"/>
<path fill="#1b1b1f" d="M78.2 22.17a4.5 4.5 0 0 1-2.27-.55 3.7 3.7 0 0 1-1.46-1.55 5.2 5.2 0 0 1-.5-2.38c0-.9.16-1.69.5-2.37a3.87 3.87 0 0 1 3.62-2.16c.54 0 1.05.09 1.53.26s.9.44 1.27.8.66.81.87 1.37c.22.55.32 1.21.32 1.98v.63h-7.15v-1.4h5.18c0-.4-.09-.74-.25-1.05-.17-.3-.4-.55-.7-.73s-.64-.27-1.04-.27a2 2 0 0 0-1.1.3c-.32.2-.57.47-.75.8q-.255.495-.27 1.08v1.2c0 .52.1.95.28 1.31.19.36.45.64.78.83.33.2.72.29 1.17.29.3 0 .57-.04.81-.13.25-.08.46-.21.63-.38.18-.16.32-.37.4-.62l1.93.22a3 3 0 0 1-.7 1.33c-.33.37-.76.67-1.3.87-.52.2-1.12.31-1.8.31zM67.5 22.15a3.3 3.3 0 0 1-3.13-2.06 5.7 5.7 0 0 1-.47-2.44q0-1.455.48-2.46a3.34 3.34 0 0 1 4.39-1.77c.33.17.59.38.78.62.2.24.34.46.45.68h.08v-4.36h2.07V22h-2.03v-1.38H70c-.1.22-.26.44-.46.68a2.53 2.53 0 0 1-2.04.85m.57-1.68c.44 0 .8-.12 1.12-.36.3-.23.54-.57.7-1 .15-.41.23-.9.23-1.47s-.08-1.06-.24-1.48a2.1 2.1 0 0 0-.69-.97 1.8 1.8 0 0 0-1.12-.34c-.46 0-.84.12-1.15.35-.3.24-.53.57-.69 1-.15.41-.23.9-.23 1.44s.08 1.03.23 1.46c.16.42.4.76.7 1 .31.24.7.37 1.14.37M60.09 22v-8.73h2.05V22zm1.03-9.97a1.2 1.2 0 0 1-1.101-.677 1 1 0 0 1-.09-.433c0-.31.12-.57.35-.79q.36-.33.84-.33.495 0 .84.33a1.045 1.045 0 0 1 0 1.58c-.23.22-.5.32-.84.32M54.66 22.17a3.83 3.83 0 0 1-3.68-2.16 5.24 5.24 0 0 1-.5-2.34c0-.89.17-1.67.51-2.35s.82-1.2 1.44-1.59a4.16 4.16 0 0 1 2.22-.57c.7 0 1.33.13 1.88.4a3.21 3.21 0 0 1 1.87 2.74h-1.97a1.88 1.88 0 0 0-.56-1.06q-.45-.42-1.2-.42c-.41 0-.78.11-1.1.34-.32.22-.57.54-.74.96-.18.43-.27.93-.27 1.52 0 .6.1 1.1.27 1.53.17.42.41.75.73.98s.69.34 1.12.34c.3 0 .57-.05.81-.17.24-.11.45-.28.6-.5.17-.22.28-.49.34-.8h1.97a3.23 3.23 0 0 1-1.83 2.76 4.3 4.3 0 0 1-1.9.39zM46.68 18.33v-5.06h2.06V22h-2v-1.55h-.09c-.2.49-.52.89-.97 1.2-.45.3-1 .46-1.65.46a2.64 2.64 0 0 1-2.54-1.5 3.95 3.95 0 0 1-.37-1.78v-5.56h2.06v5.24c0 .55.15 1 .45 1.32.3.33.7.49 1.2.49a1.89 1.89 0 0 0 1.57-.88c.19-.3.28-.67.28-1.11M32.15 22V10.36h2.1v9.87h5.13V22z"/>
<path stroke="#1b1b1f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 16a4 4 0 1 0-8 0 8 8 0 0 0 16 0c0-3.55-1.55-6.75-4-8.94"/>
<path stroke="#f56565" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 16a4 4 0 1 0 8 0 8 8 0 0 0-16 0c0 3.58 1.57 6.8 4.06 9"/>
<path fill="#ddd" d="M92 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4"/>
<path fill="#3178c6" d="M122 6h-20v20h20z"/>
<path fill="#fff" fill-rule="evenodd" d="M113.758 21.664v2.157q.526.267 1.242.406.751.14 1.516.133c.496.003.991-.047 1.476-.15a3.7 3.7 0 0 0 1.227-.49 2.54 2.54 0 0 0 .836-.893q.303-.55.304-1.358c0-.392-.054-.735-.172-1.032a2.5 2.5 0 0 0-.508-.789 3.7 3.7 0 0 0-.788-.616 9 9 0 0 0-1.04-.517 10 10 0 0 1-.765-.343 3.4 3.4 0 0 1-.57-.336 1.5 1.5 0 0 1-.367-.367.77.77 0 0 1-.125-.437q0-.223.117-.399c.077-.117.187-.219.32-.305.14-.086.312-.148.515-.195q.308-.07.672-.07c.18 0 .367.015.57.039.203.023.399.07.602.125q.303.082.594.21c.187.086.367.188.531.297V14.72a5.5 5.5 0 0 0-1.078-.282 8.5 8.5 0 0 0-1.336-.093q-.771 0-1.461.164a3.7 3.7 0 0 0-1.211.515 2.6 2.6 0 0 0-.828.892c-.203.358-.305.796-.305 1.296 0 .641.188 1.187.555 1.648q.563.678 1.687 1.157c.298.125.571.241.828.359.259.117.477.234.665.367.187.133.335.266.445.415s.164.32.164.508a.76.76 0 0 1-.101.39 1.1 1.1 0 0 1-.305.312 1.4 1.4 0 0 1-.516.204 3.3 3.3 0 0 1-.718.07 4.1 4.1 0 0 1-1.392-.25 4 4 0 0 1-1.28-.727m-3.594-5.375h2.773v-1.772h-7.734v1.773h2.758v7.914h2.195V16.29z" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -48,8 +48,6 @@
"@types/yargs": "^17.0.33", "@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "4.0.12",
"@vitest/ui": "4.0.12",
"ajv-cli": "^5.0.0", "ajv-cli": "^5.0.0",
"dotenv": "^17.0.0", "dotenv": "^17.0.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
@@ -61,7 +59,6 @@
"eslint-import-resolver-typescript": "^3.10.1", "eslint-import-resolver-typescript": "^3.10.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"jsdom": "^27.3.0",
"lint-staged": "^13.3.0", "lint-staged": "^13.3.0",
"minimist": "^1.2.8", "minimist": "^1.2.8",
"openai": "^5.8.1", "openai": "^5.8.1",
@@ -72,7 +69,6 @@
"simple-git": "^3.27.0", "simple-git": "^3.27.0",
"svgo": "^3.3.2", "svgo": "^3.3.2",
"svgson": "^5.3.1", "svgson": "^5.3.1",
"vitest": "4.0.12",
"yargs": "^17.7.2", "yargs": "^17.7.2",
"zod": "^3.25.67" "zod": "^3.25.67"
}, },
@@ -89,13 +85,13 @@
} }
}, },
"overrides": { "overrides": {
"axios": "^1.12.0",
"cross-spawn": "7.0.5", "cross-spawn": "7.0.5",
"fast-json-patch": "^3.1.1",
"form-data": "^4.0.4", "form-data": "^4.0.4",
"fast-json-patch": "^3.1.1",
"webpack-dev-middleware": "^5.3.4",
"semver": "^7.7.3", "semver": "^7.7.3",
"vite-prerender-plugin": "0.5.12", "axios": "^1.12.0",
"webpack-dev-middleware": "^5.3.4" "vite-prerender-plugin": "0.5.12"
} }
} }
} }

View File

@@ -1,38 +0,0 @@
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: {},
},
],
};

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,187 +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 `provideLucideIcons()`, 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 { BrowserModule, NgModule } from '@angular/core';
import { LucideAngularModule, AirVent, AlarmClock } from 'lucide-angular';
@NgModule({
imports: [
BrowserModule,
LucideAngularModule.pick({ AirVent, AlarmClock }),
],
})
export class AppModule {}
```
#### Standalone
```ts
import { ApplicationConfig } from '@angular/core';
import { LucideAngularModule, AirVent, AlarmClock } from 'lucide-angular';
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

@@ -1,51 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "pnpm"
},
"newProjectRoot": ".",
"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"]
}
}
}
}
}
}

View File

@@ -1,7 +0,0 @@
{
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "./dist",
"lib": {
"entryFile": "./src/public-api.ts"
}
}

View File

@@ -1,73 +0,0 @@
{
"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/angular"
},
"publishConfig": {
"directory": "dist"
},
"scripts": {
"ng": "ng",
"watch": "ng build --watch --configuration development",
"prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons",
"build": "pnpm prebuild && 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:ng": "ng build --configuration production",
"test": "pnpm prebuild && 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:*",
"@lucide/helpers": "workspace:*",
"@vitest/browser-playwright": "^4.0.12",
"angular-eslint": "21.1.0",
"ng-packagr": "^21.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"typescript": "~5.9.2"
},
"peerDependencies": {
"@angular/common": "17.x - 21.x",
"@angular/core": "17.x - 21.x"
}
}

View File

@@ -1,68 +0,0 @@
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,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
aliases = [],
}) => {
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 ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName}
* @see https://lucide.dev/guide/packages/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 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) => {
return `
/**
* @deprecated
* @see ${angularComponentName}
*/
export const ${aliasComponentName} = ${angularComponentName};
`;
}).join(`\n\n`)}
`;
});

View File

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

View File

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

View File

@@ -1,67 +0,0 @@
import { InjectionToken, Provider } from '@angular/core';
/**
* 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,
strokeWidth: 2,
absoluteStrokeWidth: false,
};
/**
* 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,
useValue: {
...lucideDefaultConfig,
...config,
},
};
}

View File

@@ -1,146 +0,0 @@
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]': '!title()',
},
})
export abstract class LucideIconBase {
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);
/**
* 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,
});
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');
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(() => {
elements.forEach((element) =>
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);
});
}
});
}
}

View File

@@ -1,4 +0,0 @@
@if (title(); as titleValue) {
<title>{{ titleValue }}</title>
}
<ng-content />

View File

@@ -1,243 +0,0 @@
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')).toBe('false');
});
it('should set aria-hidden if no title is provided', () => {
title.set(undefined);
fixture.detectChanges();
expect(getSvgAttribute('aria-hidden')).toBe('true');
});
});
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>');
});
});
});

View File

@@ -1,65 +0,0 @@
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;
}
/**
* Generic icon component for rendering 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());
});
protected override readonly iconName = computed(() => {
return this.resolvedIcon()?.name;
});
protected 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;
}
}

View File

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

View File

@@ -1,64 +0,0 @@
import { InjectionToken, Provider } from '@angular/core';
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 provideLucideIcons}
*/
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 {
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),
};
}
}

View File

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

View File

@@ -1,47 +0,0 @@
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 };
/**
* 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>>;
}> & {
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 &&
'iconData' in icon &&
Array.isArray(icon.iconData) &&
'iconName' in icon &&
typeof icon.iconName === 'string'
);
}
export type LucideIconInput = LucideIconComponentType | LucideIconData | string;
/**
* @internal
*/
export type Nullable<T> = T | null | undefined;

View File

@@ -1,3 +0,0 @@
export function formatFixed(number: number, decimals = 3): string {
return parseFloat(number.toFixed(decimals)).toString(10);
}

View File

@@ -1,2 +0,0 @@
export const toKebabCase = (name: string) =>
name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
/* 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"
}
}

View File

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

View File

@@ -56,6 +56,7 @@
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.6", "vite": "^6.3.6",
"vitest": "^4.0.12",
"astro": "^5.16.0" "astro": "^5.16.0"
}, },
"peerDependencies": { "peerDependencies": {

10
packages/icons/.npmignore Normal file
View File

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

View File

@@ -1,17 +1,16 @@
<p align="center"> <p align="center">
<a href="https://github.com/lucide-icons/lucide"> <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"> <img src="https://lucide.dev/package-logos/icons.svg" alt="" width="540">
</a> </a>
</p> </p>
<p align="center"> <p align="center">
Lucide icon library for Angular applications. Lucide helper library that exports icon data.
</p> </p>
<div align="center"> <div align="center">
[![npm](https://img.shields.io/npm/v/@lucide/icons?color=blue)](https://www.npmjs.com/package/@lucide/icons)
[![npm](https://img.shields.io/npm/v/@lucide/angular?color=blue)](https://www.npmjs.com/package/@lucide/angular) ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/icons)
![NPM Downloads](https://img.shields.io/npm/dw/@lucide/angular)
[![GitHub](https://img.shields.io/github/license/lucide-icons/lucide)](https://lucide.dev/license) [![GitHub](https://img.shields.io/github/license/lucide-icons/lucide)](https://lucide.dev/license)
</div> </div>
@@ -20,40 +19,46 @@ Lucide icon library for Angular applications.
· ·
<a href="https://lucide.dev/icons/">Icons</a> <a href="https://lucide.dev/icons/">Icons</a>
· ·
<a href="https://lucide.dev/guide/packages/angular">Documentation</a> <a href="https://lucide.dev/guide/packages/lucide">Documentation</a>
· ·
<a href="https://lucide.dev/license">License</a> <a href="https://lucide.dev/license">License</a>
</p> </p>
# Lucide Angular # @lucide/icons
A standalone, signal based, zoneless implementation of the Lucide icon library for Angular applications. A helper library that exports Lucide icon data in a tree-shakable format, also providing utilities for dynamic importing icons.
## Installation ## Installation
```sh ```sh
pnpm add @lucide/angular pnpm add @lucide/icons
``` ```
```sh ```sh
npm install @lucide/angular npm install @lucide/icons
``` ```
```sh ```sh
yarn add @lucide/angular yarn add @lucide/icons
``` ```
```sh ```sh
bun add @lucide/angular bun add @lucide/icons
```
### CDN
```html
<!-- Development version -->
<script src="https://unpkg.com/@lucide/icons@latest/dist/umd/lucide.js"></script>
<!-- Production version -->
<script src="https://unpkg.com/@lucide/icons@latest"></script>
``` ```
## Documentation ## Documentation
For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/angular) For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/icons)
## Migration guide
Migrating from `lucide-angular`? Read our [comprehensive migration guide](./MIGRATION.md).
## Community ## Community
@@ -74,4 +79,4 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
### Awesome backers 🍺 ### 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://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> <a href="https://github.com/pdfme/pdfme"><img src="../../docs/public/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a>

View File

@@ -0,0 +1,77 @@
{
"name": "@lucide/icons",
"description": "A Lucide icon library that contains icon data in a standard Lucide format.",
"version": "0.0.1",
"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"
},
"keywords": [
"Lucide",
"HTML",
"Feather",
"Icons",
"Icon",
"SVG",
"Feather Icons"
],
"amdName": "lucide-icons",
"source": "src/lucide-icons.ts",
"main": "dist/cjs/lucide-icons.cjs",
"module": "dist/esm/lucide-icons.mjs",
"types": "dist/esm/lucide-icons.d.ts",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/esm/lucide-icons.d.ts",
"import": "./dist/esm/lucide-icons.mjs",
"require": "./dist/cjs/lucide-icons.cjs",
"default": "./dist/esm/lucide-icons.mjs"
},
"./icons/*": {
"import": "./dist/esm/icons/*.mjs",
"default": "./dist/esm/icons/*.mjs"
},
"./dynamic": {
"types": "./dist/esm/dynamic.d.ts",
"import": "./dist/esm/dynamic.mjs",
"require": "./dist/cjs/dynamic.cjs",
"default": "./dist/esm/dynamic.mjs"
},
"./build": {
"types": "./dist/esm/build.d.ts",
"import": "./dist/esm/build.mjs",
"require": "./dist/cjs/build.cjs",
"default": "./dist/esm/build.mjs"
}
},
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundle",
"copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --withAliases --withDynamicImports --separateAliasesFile --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts",
"build:bundle": "rollup -c rollup.config.mjs",
"test": "pnpm build:icons && vitest run",
"test:watch": "vitest watch",
"test:update": "vitest -u",
"version": "pnpm version --git-tag-version=false"
},
"devDependencies": {
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "^1.0.0",
"@lucide/rollup-plugins": "workspace:*",
"@rollup/plugin-replace": "^6.0.2",
"@testing-library/jest-dom": "^6.6.3",
"jest-serializer-html": "^7.1.0",
"rollup": "^4.53.3",
"rollup-plugin-dts": "^6.2.3",
"rollup-plugin-preserve-directives": "^0.4.0",
"typescript": "^5.8.3",
"vite": "^7.2.4",
"vitest": "^4.0.12"
}
}

View File

@@ -0,0 +1,87 @@
import plugins from '@lucide/rollup-plugins';
import pkg from './package.json' with { type: 'json' };
import dts from 'rollup-plugin-dts';
const packageName = '@lucide/icons';
const outputFileName = 'lucide-icons';
const inputs = [`src/lucide-icons.ts`];
const bundles = [
{
format: 'cjs',
inputs,
extension: 'cjs',
},
{
format: 'esm',
inputs: [...inputs, './src/dynamic.ts', './src/build.ts'],
preserveModules: true,
extension: 'mjs',
},
];
const configs = bundles
.map(
({
inputs,
outputDir = 'dist',
outputFile,
format,
minify,
preserveModules,
entryFileNames,
external = [],
paths,
extension = 'js',
}) =>
inputs.map((input) => ({
input,
plugins: [...plugins({ pkg, minify })],
external,
output: {
name: packageName,
entryFileNames,
...(preserveModules
? {
dir: `${outputDir}/${format}`,
entryFileNames: `[name].${extension}`,
}
: {
file:
outputFile ??
`${outputDir}/${format}/${outputFileName}${minify ? '.min' : ''}.${extension}`,
}),
paths,
format,
sourcemap: true,
preserveModules,
exports: 'named',
globals: {},
preserveModulesRoot: 'src',
},
})),
)
.flat();
export default [
...[
outputFileName,
`${outputFileName}.prefixed`,
`${outputFileName}.suffixed`,
'dynamic',
'build',
].map((filename) => ({
input: `./src/${filename}.ts`,
output: [
{
file: `dist/esm/${filename}.d.ts`,
format: 'esm',
},
{
file: `dist/cjs/${filename}.d.cts`,
format: 'cjs',
},
],
plugins: [dts()],
})),
...configs,
];

View File

@@ -0,0 +1,36 @@
/* eslint-disable import/no-extraneous-dependencies */
import base64SVG from '@lucide/build-icons/utils/base64SVG';
export default async ({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
return `
import type { LucideIcon } from '../types';
/**
* @name ${iconName}
* @description Lucide SVG icon node.
*
* @preview ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName}
* @see https://lucide.dev/guide/packages/lucide - Documentation
*
* @returns {Array}
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName}: LucideIcon = ${JSON.stringify({
name: iconName,
size: 24,
node: children,
})}
export default ${componentName};
`;
};

View File

@@ -0,0 +1,3 @@
export * from './aliases';
export * from './prefixed';
export * from './suffixed';

View File

@@ -0,0 +1,4 @@
export { default as buildLucideIconNode } from './buildLucideIconNode';
export { default as buildLucideDataUri } from './buildLucideDataUri';
export { default as buildLucideIconElement } from './buildLucideIconElement';
export { default as buildLucideSvg } from './buildLucideSvg';

View File

@@ -0,0 +1,30 @@
import { LucideBuildParams, LucideIcon } from './types';
import buildLucideSvg from './buildLucideSvg';
/**
* Creates a base64 encoded data URI from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideDataUri(icon: LucideIcon, params: LucideBuildParams = {}) {
const svg = buildLucideSvg(icon, params);
// Browser
if (typeof btoa === 'function') {
// Ensure proper UTF-8 handling before base64
const utf8Bytes = new TextEncoder().encode(svg);
let binary = '';
for (const element of utf8Bytes) binary += String.fromCodePoint(element);
return `data:image/svg+xml;base64,${btoa(binary)}`;
}
// Node.js (and other JS runtimes with Buffer)
if (typeof Buffer !== 'undefined') {
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`;
}
throw new Error('No base64 encoder available in this environment.');
}
export default buildLucideDataUri;

View File

@@ -0,0 +1,34 @@
import { LucideBuildParams, LucideIcon, LucideIconNode } from './types';
import buildLucideIconNode from './buildLucideIconNode';
const buildDomElement = (
document: Document,
[tagName, attributes, children = []]: LucideIconNode,
): Element => {
const element = document.createElementNS('http://www.w3.org/2000/svg', tagName);
for (const [attrName, value] of Object.entries(attributes)) {
element.setAttribute(attrName, value);
}
for (const node of children) {
const childNode = buildDomElement(document, node);
element.appendChild(childNode);
}
return element;
};
/**
* Creates an SvgElement from a Lucide icon object.
*
* @param document The document to create the Element in.
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconElement(
document: Document,
icon: LucideIcon,
params: LucideBuildParams = {},
) {
return buildDomElement(document, buildLucideIconNode(icon, params));
}
export default buildLucideIconElement;

View File

@@ -0,0 +1,50 @@
import { LucideBuildParams, LucideIcon, LucideIconNode } from './types';
const defaultAttributes = {
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',
};
/**
* Creates a Lucide icon node (an svgson-like format) from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideIconNode(icon: LucideIcon, params: LucideBuildParams = {}): LucideIconNode {
const viewBoxWidth = ('size' in icon ? icon.size : icon.width) ?? defaultAttributes.width;
const viewBoxHeight = ('size' in icon ? icon.size : icon.height) ?? defaultAttributes.height;
const attributes = {
...defaultAttributes,
...('color' in params && { stroke: params['color'] }),
...('size' in params &&
params['size'] && {
width: params['size'].toString(10),
height: params['size'].toString(10),
}),
...('width' in params && params['width'] && { width: params['width'].toString(10) }),
...('height' in params && params['height'] && { height: params['height'].toString(10) }),
...('strokeWidth' in params &&
params['strokeWidth'] && { 'stroke-width': params['strokeWidth'].toString(10) }),
class: `lucide lucide-${icon.name} ${params['className'] ?? ''}`.trim(),
viewBox: `0 0 ${viewBoxWidth} ${viewBoxHeight}`,
...('attributes' in params && params.attributes),
};
return [
'svg',
attributes,
icon.node.map(([name, attrs]) => [
name,
params['absoluteStrokeWidth'] ? { 'vector-effect': 'non-scaling-stroke', ...attrs } : attrs,
]),
];
}
export default buildLucideIconNode;

View File

@@ -0,0 +1,19 @@
import { LucideBuildParams, LucideIcon, LucideIconNode } from './types';
import buildLucideIconNode from './buildLucideIconNode';
const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): string =>
`<${tagName} ${Object.entries(attributes)
.map(([attrName, value]) => `${attrName}="${value}"`)
.join(' ')}>${children?.map((child) => buildDomNode(child)).join('')}</${tagName}>`;
/**
* Creates an SVG string from a Lucide icon object.
*
* @param icon The icon to build.
* @param params Additional build parameters.
*/
function buildLucideSvg(icon: LucideIcon, params: LucideBuildParams = {}) {
return buildDomNode(buildLucideIconNode(icon, params));
}
export default buildLucideSvg;

View File

@@ -0,0 +1,2 @@
export { lucideIconNames, type LucideIconName } from './dynamicIcon';
export { default as lucideDynamicIconImports } from './dynamicIconImports';

View File

@@ -0,0 +1,8 @@
import dynamicIconImports from './dynamicIconImports';
export type LucideIconName = keyof typeof dynamicIconImports;
/**
* The list of available Lucide icon names.
*/
export const lucideIconNames = Object.keys(dynamicIconImports) as Array<LucideIconName>;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
export type SVGProps = Record<string, string>;
/**
* A Lucide icon node (an svgson-like internal format)
*/
export type LucideIconNode =
| [attrName: string, attributes: SVGProps]
| [attrName: string, attributes: SVGProps, children: LucideIconNode[]];
/**
* A Lucide icon object that fully describes an icon to be displayed.
*/
export type LucideIcon = {
name: string;
node: LucideIconNode[];
} & ({ size: number } | { width: number; height: number });
/**
* Build parameters for creating a Lucide icon instance for display.
*/
export type LucideBuildParams = {
/**
* The color of the icon.
*/
color?: string;
/**
* The stroke width.
*/
strokeWidth?: number;
/**
* Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements.
*/
absoluteStrokeWidth?: boolean;
/**
* Extra CSS class names to pass to the SVG element.
*/
className?: string;
/**
* Any extra attributes to pass to the SVG element.
*/
attributes?: SVGProps;
} & ({ size?: number } | { width?: number; height?: number });

View File

@@ -0,0 +1,24 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`buildLucideIconElement > should match the snapshot 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewbox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-house"
>
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"
key="5wwlr5"
>
</path>
<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
key="r6nss1"
>
</path>
</svg>
`;

View File

@@ -0,0 +1,3 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`buildLucideIconNode > should match the snapshot 1`] = `"{"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","class":"lucide lucide-house"}"`;

View File

@@ -0,0 +1,26 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`buildLucideSvg > should match the snapshot 1`] = `
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewbox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-house"
>
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"
key="5wwlr5"
>
</path>
<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
key="r6nss1"
>
</path>
</svg>
`;
exports[`buildLucideSvgDataUri > should match the snapshot 1`] = `"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWhvdXNlIj48cGF0aCBkPSJNMTUgMjF2LThhMSAxIDAgMCAwLTEtMWgtNGExIDEgMCAwIDAtMSAxdjgiIGtleT0iNXd3bHI1Ij48L3BhdGg+PHBhdGggZD0iTTMgMTBhMiAyIDAgMCAxIC43MDktMS41MjhsNy02YTIgMiAwIDAgMSAyLjU4MiAwbDcgNkEyIDIgMCAwIDEgMjEgMTB2OWEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnoiIGtleT0icjZuc3MxIj48L3BhdGg+PC9zdmc+"`;

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { House } from '../src/lucide-icons';
import { getOriginalSvg, removeKeys } from './helpers';
import buildLucideIconElement from '../src/buildLucideIconElement';
describe('buildLucideIconElement', () => {
it('should create SVG Element', () => {
const HomeSVG = buildLucideIconElement(document, House);
expect(HomeSVG.tagName).toBe('svg');
});
it('should match the snapshot', () => {
const HomeSVG = buildLucideIconElement(document, House);
expect(HomeSVG.outerHTML).toMatchSnapshot();
});
it('should create SVG Element with attributes', () => {
const HomeSVG = buildLucideIconElement(document, House, { attributes: { fill: 'red' } });
expect(HomeSVG.getAttribute('fill')).toBe('red');
});
it('should create SVG Element with class name', () => {
const HomeSVG = buildLucideIconElement(document, House, { attributes: { class: 'icon' } });
expect(HomeSVG.getAttribute('class')).toBe('icon');
});
it('should merge classes', () => {
const HomeSVG = buildLucideIconElement(document, House, { className: 'icon' });
expect(HomeSVG.getAttribute('class')).toBe('lucide lucide-house icon');
});
it('should create the correct svg element', () => {
const HomeSVG = buildLucideIconElement(document, House);
const svg = getOriginalSvg('house', undefined, true);
expect(removeKeys(HomeSVG.outerHTML)).toBe(svg);
});
});

View File

@@ -0,0 +1,86 @@
import { describe, expect, it } from 'vitest';
import { House } from '../src/lucide-icons';
import buildLucideIconNode from '../src/buildLucideIconNode';
describe('buildLucideIconNode', () => {
it('should create icon node', () => {
const HouseSVG = buildLucideIconNode(House);
expect(HouseSVG.at(0)).toBe('svg');
});
it('should match the snapshot', () => {
const HouseSVG = buildLucideIconNode(House);
expect(JSON.stringify(HouseSVG.at(1))).toMatchSnapshot();
});
it('should override dimensions, but not viewBox', () => {
const HouseSVG = buildLucideIconNode(House, { size: 12 });
expect(HouseSVG[1]['width']).toBe('12');
expect(HouseSVG[1]['height']).toBe('12');
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
});
it('should override width, but not height', () => {
const HouseSVG = buildLucideIconNode(House, { width: 12 });
expect(HouseSVG[1]['width']).toBe('12');
expect(HouseSVG[1]['height']).toBe('24');
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
});
it('should override height, but not width', () => {
const HouseSVG = buildLucideIconNode(House, { height: 12 });
expect(HouseSVG[1]['width']).toBe('24');
expect(HouseSVG[1]['height']).toBe('12');
expect(HouseSVG[1]['viewBox']).toBe('0 0 24 24');
});
it('should override color', () => {
const HouseSVG = buildLucideIconNode(House, { color: 'pink' });
expect(HouseSVG[1]['stroke']).toBe('pink');
expect(HouseSVG[1]['fill']).toBe('none');
});
it('should override stroke width', () => {
const HouseSVG = buildLucideIconNode(House, { strokeWidth: 12 });
expect(HouseSVG[1]['stroke-width']).toBe('12');
});
it('should set non-scaling-stroke to child nodes', () => {
const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: true });
for (const node of HouseSVG[2]!) {
expect(node[1]['vector-effect']).toBe('non-scaling-stroke');
}
});
it('should not set non-scaling-stroke', () => {
const HouseSVG = buildLucideIconNode(House, { absoluteStrokeWidth: false });
expect(HouseSVG[1]['vector-effect']).toBeUndefined();
});
it('should create icon node with attributes', () => {
const HouseSVG = buildLucideIconNode(House, { attributes: { fill: 'red' } });
expect(HouseSVG[1]['fill']).toBe('red');
});
it('should create icon node with class name', () => {
const HouseSVG = buildLucideIconNode(House, { attributes: { class: 'icon' } });
expect(HouseSVG[1]['class']).toBe('icon');
});
it('should merge classes', () => {
const HouseSVG = buildLucideIconNode(House, { className: 'icon' });
expect(HouseSVG[1]['class']).toBe('lucide lucide-house icon');
});
});

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';
import { House } from '../src/lucide-icons';
import { getOriginalSvg, removeKeys } from './helpers';
import buildLucideSvg from '../src/buildLucideSvg';
import buildLucideDataUri from '../src/buildLucideDataUri';
describe('buildLucideSvg', () => {
it('should match the snapshot', () => {
const HouseSVG = buildLucideSvg(House);
expect(HouseSVG).toMatchSnapshot();
});
it('should create the correct svg element', () => {
const HouseSVG = buildLucideSvg(House);
const svg = getOriginalSvg('house', undefined, true);
expect(removeKeys(HouseSVG)).toBe(svg);
});
});
describe('buildLucideSvgDataUri', () => {
it('should match the snapshot', () => {
const HouseDataUri = buildLucideDataUri(House);
expect(HouseDataUri).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,18 @@
import fs from 'fs';
import path from 'path';
import { parseSync, stringify } from 'svgson';
const ICONS_DIR = path.resolve(__dirname, '../../../icons');
export const getOriginalSvg = (iconName: string, aliasName?: string, setAttrs = true) => {
const svgContent = fs.readFileSync(path.join(ICONS_DIR, `${iconName}.svg`), 'utf8');
const svgParsed = parseSync(svgContent);
if (setAttrs) {
svgParsed.attributes['class'] = `lucide lucide-${aliasName ?? iconName}`;
}
return stringify(svgParsed, { selfClose: false });
};
export const removeKeys = (svg: string) => svg.replaceAll(/ key="[^"]+"/g, '');

View File

@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest';
import { House } from '../src/lucide-icons';
describe('lucide-icons', () => {
it('should init', () => {
const HouseSVG = House;
expect(House).toMatchObject(House);
});
});

View File

@@ -0,0 +1,5 @@
import { expect } from 'vitest';
import '@testing-library/jest-dom/vitest';
import htmlSerializer from 'jest-serializer-html';
expect.addSnapshotSerializer(htmlSerializer);

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"strict": true,
"declaration": false,
"noEmitOnError": true,
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"module": "ESNext",
"target": "ESNext",
"esModuleInterop": true,
"isolatedModules": true,
"lib": ["esnext", "dom"],
"skipLibCheck": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"sourceMap": true,
"outDir": "./dist",
},
"exclude": ["**/node_modules"],
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setupVitest.js',
}
});

View File

@@ -51,7 +51,8 @@
"rollup": "^4.53.3", "rollup": "^4.53.3",
"rollup-plugin-dts": "^6.2.3", "rollup-plugin-dts": "^6.2.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.0.12"
}, },
"peerDependencies": { "peerDependencies": {
"preact": "^10.27.2" "preact": "^10.27.2"

View File

@@ -71,7 +71,8 @@
"rollup": "^4.53.3", "rollup": "^4.53.3",
"rollup-plugin-dts": "^6.2.3", "rollup-plugin-dts": "^6.2.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.0.12"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",

View File

@@ -63,7 +63,8 @@
"rollup-plugin-dts": "^6.2.3", "rollup-plugin-dts": "^6.2.3",
"rollup-plugin-preserve-directives": "^0.4.0", "rollup-plugin-preserve-directives": "^0.4.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.0.12"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@@ -82,6 +82,7 @@
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4", "vite": "^7.2.4",
"vite-plugin-solid": "^2.11.6", "vite-plugin-solid": "^2.11.6",
"vitest": "^4.0.12",
"esbuild": "^0.25.0" "esbuild": "^0.25.0"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -48,7 +48,7 @@
"scripts": { "scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:package && pnpm build:license", "build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:package && pnpm build:license",
"copy:license": "cp ../../LICENSE ./LICENSE", "copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.svelte && rm -rf ./src/icons/*.ts && rm -f index.js", "clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.svelte && rm -f index.js",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --exportFileName=index.ts --iconFileExtension=.svelte --importImportFileExtension=.svelte --separateIconFileExport --separateIconFileExportExtension=.ts --withAliases --aliasesFileExtension=.ts --separateAliasesFile --separateAliasesFileExtension=.ts --aliasImportFileExtension=.js --pretty=false", "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --exportFileName=index.ts --iconFileExtension=.svelte --importImportFileExtension=.svelte --separateIconFileExport --separateIconFileExportExtension=.ts --withAliases --aliasesFileExtension=.ts --separateAliasesFile --separateAliasesFileExtension=.ts --aliasImportFileExtension=.js --pretty=false",
"build:package": "svelte-package --input ./src", "build:package": "svelte-package --input ./src",
"build:license": "node ./scripts/appendBlockComments.mts", "build:license": "node ./scripts/appendBlockComments.mts",
@@ -65,11 +65,13 @@
"@testing-library/svelte": "^4.0.2", "@testing-library/svelte": "^4.0.2",
"@tsconfig/svelte": "^5.0.0", "@tsconfig/svelte": "^5.0.0",
"jest-serializer-html": "^7.1.0", "jest-serializer-html": "^7.1.0",
"jsdom": "^20.0.3",
"svelte": "^4.2.19", "svelte": "^4.2.19",
"svelte-check": "^3.4.4", "svelte-check": "^3.4.4",
"svelte-preprocess": "^5.0.4", "svelte-preprocess": "^5.0.4",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.0.12"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^3 || ^4 || ^5.0.0-next.42" "svelte": "^3 || ^4 || ^5.0.0-next.42"

View File

@@ -53,6 +53,7 @@
"rollup": "^4.53.3", "rollup": "^4.53.3",
"rollup-plugin-dts": "^6.2.3", "rollup-plugin-dts": "^6.2.3",
"vite": "^7.2.4", "vite": "^7.2.4",
"vitest": "^4.0.12",
"vue": "^3.4.21" "vue": "^3.4.21"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -51,6 +51,7 @@
"rollup": "^4.53.3", "rollup": "^4.53.3",
"rollup-plugin-dts": "^6.2.3", "rollup-plugin-dts": "^6.2.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.0.12"
} }
} }

View File

@@ -13,6 +13,7 @@
"test:watch": "vitest watch" "test:watch": "vitest watch"
}, },
"devDependencies": { "devDependencies": {
"vite": "^7.2.4" "vite": "^7.2.4",
"vitest": "^4.0.12"
} }
} }

View File

@@ -65,11 +65,13 @@
"@testing-library/svelte": "^5.2.7", "@testing-library/svelte": "^5.2.7",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"jest-serializer-html": "^7.1.0", "jest-serializer-html": "^7.1.0",
"jsdom": "^20.0.3",
"svelte": "^5.20.5", "svelte": "^5.20.5",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.6" "vite": "^6.3.6",
"vitest": "^4.0.12"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^5" "svelte": "^5"

View File

@@ -53,6 +53,7 @@
"rollup-plugin-dts": "^6.1.0", "rollup-plugin-dts": "^6.1.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.2.4", "vite": "^7.2.4",
"vitest": "^4.0.12",
"vue": "^3.4.21" "vue": "^3.4.21"
}, },
"peerDependencies": { "peerDependencies": {

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

View File

@@ -48,11 +48,7 @@ function generateIconFiles({
]); ]);
const getSvg = () => readSvg(`${iconName}.svg`, iconsDir); const getSvg = () => readSvg(`${iconName}.svg`, iconsDir);
const { const { deprecated = false, toBeRemovedInVersion = undefined } = iconMetaData[iconName];
deprecated = false,
toBeRemovedInVersion = undefined,
aliases = [],
} = iconMetaData[iconName];
const deprecationReason = deprecated const deprecationReason = deprecated
? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', { ? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', {
componentName, componentName,
@@ -68,7 +64,6 @@ function generateIconFiles({
getSvg, getSvg,
deprecated, deprecated,
deprecationReason, deprecationReason,
aliases,
}); });
const output = pretty const output = pretty
@@ -76,7 +71,7 @@ function generateIconFiles({
singleQuote: true, singleQuote: true,
trailingComma: 'all', trailingComma: 'all',
printWidth: 100, printWidth: 100,
parser: iconFileExtension.endsWith('.ts') ? 'babel-ts' : 'babel', parser: 'babel',
}) })
: elementTemplate; : elementTemplate;

View File

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

View File

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

View File

@@ -1,4 +1,15 @@
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;
}
export type TemplateFunction = (params: ExportTemplate) => Promise<string>;
const defineExportTemplate = (exportFunction: TemplateFunction) => exportFunction; const defineExportTemplate = (exportFunction: TemplateFunction) => exportFunction;