Compare commits

..
Author SHA1 Message Date
Karsa c8dda3d4e7 Merge branch 'main' into package/angularv17 2025-12-19 09:08:54 +01:00
Karsa a83fba9ecb fix(packages/angular): move to packages/angular, update github actions and templates 2025-12-19 09:07:20 +01:00
Karsa 2a9f3fa72e fix(packages/angular): revert changes made to lucide-angular 2025-12-17 21:47:09 +01:00
Karsa ae1ca07e36 fix(packages/angular-next): added angular-next package keeping original package intact 2025-12-17 21:46:02 +01:00
Karsa 818d99f41e Merge branch 'refs/heads/main' into package/angularv17 2025-12-17 10:48:40 +01:00
Karsa a3e7e75b90 fix(packages/icons): finalize exportTemplate before migration to input signals & effect to build component data 2025-12-17 09:35:03 +01:00
Karsa e851a03672 fix(packages/icons): trying some other variations 2025-12-15 11:53:37 +01:00
Karsa 0abfa2f0d5 Merge branch 'refs/heads/main' into package/angularv17
# Conflicts:
#	packages/lucide-angular/package.json
#	packages/lucide-angular/scripts/exportTemplate.mts
#	pnpm-lock.yaml
#	tools/build-icons/building/generateExportsFile.ts
#	tools/build-icons/building/generateIconFiles.ts
2025-12-15 10:05:13 +01:00
Karsa 6c1e34df19 feat(packages): angular v17 dead end 2025-04-19 17:15:08 +02:00
Karsa 669f62bb64 Merge branch 'refs/heads/main' into package/icons 2025-04-19 12:09:52 +02:00
Karsa 708d5114d6 feat(packages): added lucide icons package skeleton 2025-04-01 17:25:10 +02:00
169 changed files with 6615 additions and 3067 deletions
+6 -3
View File
@@ -13,16 +13,19 @@ body:
description: Which Lucide packages are affected? You may select more than one.
options:
- label: lucide
- label: lucide-angular
- label: lucide-angular (old version)
- label: '@lucide/angular (new version)'
- label: '@lucide/astro'
- label: lucide-flutter
- label: lucide-preact
- label: lucide-react
- label: lucide-react-native
- label: lucide-solid
- label: lucide-svelte
- label: lucide-static
- label: lucide-svelte (old version)
- label: `@lucide/svelte (new version)`
- label: lucide-vue
- label: lucide-vue-next
- label: lucide-astro
- label: Figma plugin
- label: source/main
- label: other/not relevant
@@ -13,19 +13,23 @@ body:
description: Which Lucide project do you wish this feature were added to? You may select more than one.
options:
- label: lucide
- label: lucide-angular
- label: lucide-angular (old version)
- label: '@lucide/angular (new version)'
- label: '@lucide/astro'
- label: lucide-flutter
- label: lucide-preact
- label: lucide-react
- label: lucide-react-native
- label: lucide-solid
- label: lucide-svelte
- label: lucide-static
- label: lucide-svelte (old version)
- label: `@lucide/svelte (new version)`
- label: lucide-vue
- label: lucide-vue-next
- label: lucide-astro
- label: Figma plugin
- label: all JS packages
- label: site
- label: other/not relevant
validations:
required: true
- type: textarea
+1
View File
@@ -59,6 +59,7 @@
🅰️ angular package:
- changed-files:
- any-glob-to-any-file:
- 'packages/angular/*'
- 'packages/lucide-angular/*'
# For changes in the lucide preact package
+41
View File
@@ -0,0 +1,41 @@
name: Lucide Angular checks
on:
pull_request:
paths:
- packages/angular/**
- tools/build-icons/**
- pnpm-lock.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @lucide/angular build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter @lucide/angular test
@@ -28,7 +28,7 @@ jobs:
{ [ -n "$ICON_NAME_SECTION" ] && echo "$ICON_NAME_SECTION" | grep -iqE "$SAFE_KEY|$SAFE_VALUE"; }; then
gh issue close ${{ github.event.issue.number }} \
--reason "not planned" \
--reason "not_planned" \
--comment "It looks like this request is about **${VALUE}**, which is a brand logo.
Lucide **does not accept** brand logos, and we do not plan to add them in the future. This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
+1
View File
@@ -58,6 +58,7 @@ jobs:
'lucide-preact',
'lucide-solid',
'lucide-svelte',
'@lucide/angular',
'@lucide/astro',
'@lucide/svelte',
]
-2
View File
@@ -20,8 +20,6 @@ packages/**/src/icons/*.tsx
packages/**/src/aliases/*.ts
packages/**/src/aliases.ts
!packages/**/src/aliases/index.ts
packages/**/src/utils/*.ts
!packages/shared/src/utils/*.ts
packages/**/src/dynamicIconImports.ts
packages/**/DynamicIcon.d.ts
packages/**/dynamicIconImports.js
+2 -2
View File
@@ -1,6 +1,6 @@
ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2026 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2026.
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2025.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@@ -18,7 +18,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The MIT License (MIT) (for portions derived from Feather)
Copyright (c) 2013-2026 Cole Bemis
Copyright (c) 2013-2023 Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+2 -11
View File
@@ -88,16 +88,7 @@ Thank you to all the people who contributed to Lucide!
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="docs/public/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Open Collective backers)
### Awesome backers 🍺
<a href="https://github.com/pdfme/pdfme"><img src="docs/public/sponsors/pdfme.svg" width="180" alt="pdfme – Open-source PDF generation library built with TypeScript and React." /></a>
<a href="https://www.paxhistoria.co/"><img src="docs/public/sponsors/paxhistoria.svg?" width="180" alt="Pax Historia – An alternate history sandbox game" /></a>
### Backers ☕
<a href="https://www.fina.money/"><img src="docs/public/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
<a href="https://github.com/pdfme/pdfme"><img src="docs/public/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a>
<a href="https://www.fina.money/"><img src="docs/public/sponsors/fina-money.png" width="180" alt="Fina Money sponsor badge" /></a>
-1
View File
@@ -2,7 +2,6 @@
"adobe": "Adobe",
"airplay": "AirPlay",
"amazon": "Amazon",
"android": "Android",
"angular": "Angular",
"aws": "AWS",
"azure": "Azure",
-1
View File
@@ -9,7 +9,6 @@ const description = 'Beautiful & consistent icon toolkit made by the community.'
// https://vitepress.dev/reference/site-config
export default defineConfig({
title,
titleTemplate: ':title \u2013 Lucide',
description,
cleanUrls: true,
outDir: '.vercel/output/static',
@@ -171,24 +171,5 @@
],
"source": "https://github.com/heyvito/lucide-rails",
"documentation": "https://github.com/heyvito/lucide-rails/blob/master/README.md"
},
{
"name": "lucide-web-components",
"description": "Implementation of Lucide icons in Web Components custom elements.",
"icon": "/framework-logos/web-components.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/npm/v/@dile/iconlib",
"href": "https://dile-components.com/icons/lucide-icons/"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/npm/dt/@dile/iconlib",
"href": "https://dile-components.com/icons/lucide-icons/"
}
],
"source": "https://github.com/Polydile/dile-components",
"documentation": "https://github.com/Polydile/dile-components/blob/master/packages/iconlib/README.md"
}
]
@@ -101,11 +101,9 @@ const value = computed({
}
.color-picker:hover,
.color-picker:focus-within {
.color-picker:focus {
border-color: var(--vp-c-brand);
}
.color-picker:focus-within {
background-color: var(--vp-c-bg);
background: var(--vp-c-bg-alt);
}
.color-input[value='currentColor'] {
@@ -48,6 +48,7 @@ defineProps({
.fake-input:hover,
.fake-input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.shortcut {
@@ -35,8 +35,6 @@ const updateShortcutSpacing = () => {
});
};
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
onMounted(updateShortcutSpacing);
watch(() => props.shortcut, updateShortcutSpacing);
@@ -75,7 +73,6 @@ defineExpose({
v-if="type === 'search' && modelValue"
class="clear-button"
aria-label="Clear input"
:style="{ right: isMac ? '50px' : '68px' }"
>
<Icon
:iconNode="x"
@@ -118,9 +115,7 @@ defineExpose({
.input:hover,
.input:focus {
border-color: var(--vp-c-brand);
}
.input:focus {
background-color: var(--vp-c-bg);
background: var(--vp-c-bg-alt);
}
.input.has-icon {
@@ -129,7 +124,7 @@ defineExpose({
.clear-button {
position: absolute;
right: 68px;
right: 56px;
top: 9px;
padding: 4px;
transition: background-color .25s;
@@ -152,32 +147,10 @@ defineExpose({
pointer-events: none;
}
.input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
display: none;
}
.input[type="search"]::-ms-clear {
display: none;
}
.input[type="search"]::-o-clear {
display: none;
}
.input[type="search"]::-moz-clear {
display: none;
}
@media (hover: none) {
.shortcut {
display: none;
}
.clear-button {
right: 16px !important;
}
}
</style>
@@ -190,7 +163,4 @@ defineExpose({
color: var(--vp-c-text-2);
pointer-events: none;
}
.input-wrapper > svg:has(+ .input.has-icon:focus) {
color: var(--vp-c-text-1);
}
</style>
@@ -153,7 +153,7 @@ function handleCloseDrawer() {
>
<StickyBar class="category-search">
<InputSearch
:placeholder="`Search ${icons.length} icons…`"
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@@ -129,7 +129,7 @@ function handleCloseDrawer() {
>
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons…`"
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
:shortcut="kbdSearchShortcut"
-5
View File
@@ -10,11 +10,6 @@ because they can quickly give information.
However, not everyone can understand them easily. When using icons it is very important to consider
the following aspects of accessibility.
::: info
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can make them accessible yourself by following the guidelines below.
:::
## Provide visible labels
Icons are a helpful tool to improve perception, but they aren't a replacement for text.
-12
View File
@@ -167,15 +167,3 @@ import { coconut } from '@lucide/lab';
})
export class AppModule { }
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```html
<lucide-icon name="check" aria-label="Task completed"></lucide-icon>
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-16
View File
@@ -192,19 +192,3 @@ import LucideIcon from './LucideIcon.astro';
<LucideIcon name="Menu" />
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
---
import { Check } from '@lucide/astro';
---
<Check aria-label="Task completed" />
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-16
View File
@@ -131,19 +131,3 @@ const App = () => {
export default App;
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
import { Check } from 'lucide-preact';
const App = () => {
return <Check aria-label="Task completed" />;
};
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-16
View File
@@ -109,19 +109,3 @@ const App = () => (
<DynamicIcon name="camera" color="red" size={48} />
);
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
import { Check } from 'lucide-react';
const App = () => {
return <Check aria-label="Task completed" />;
};
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-16
View File
@@ -144,19 +144,3 @@ const App = () => {
export default App;
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
import { Check } from 'lucide-solid';
const App = () => {
return <Check aria-label="Task completed" />;
};
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-16
View File
@@ -331,19 +331,3 @@ The example below imports all ES Modules, so exercise caution when using it. Imp
<LucideIcon name="Menu" />
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```svelte
<script>
import { Check } from '@lucide/svelte';
</script>
<Check aria-label="Task completed" />
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-22
View File
@@ -146,25 +146,3 @@ All other props listed above also work on the `Icon` Component.
</div>
</template>
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```vue
<script setup>
import { Check } from 'lucide-vue-next';
</script>
<template>
<Check
color="red"
:size="32"
aria-label="Task completed"
/>
</template>
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-21
View File
@@ -62,7 +62,6 @@ Here is a complete example with unpkg
</script>
</body>
```
We strongly suggest you anchor to a specific version, such as `https://unpkg.com/lucide@x.xxx.x/dist/umd/lucide.min.js`, rather than using `@latest`.
### With ESModules
@@ -204,23 +203,3 @@ createIcons({
}
});
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```html
<!DOCTYPE html>
<body>
<i data-lucide="house" aria-label="Home icon"></i>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
lucide.createIcons();
</script>
</body>
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
-1
View File
@@ -1,5 +1,4 @@
---
title: Categories
layout: page
outline: 2
outlineTitle: Categories
-1
View File
@@ -1,5 +1,4 @@
---
title: Icons
layout: page
outline: 2
outlineTitle: Categories
-1
View File
@@ -2,7 +2,6 @@
# https://vitepress.dev/reference/default-theme-home-page
layout: home
markdownStyles: false
titleTemplate: false
head:
- - link
-1
View File
@@ -1,5 +1,4 @@
---
title: License
aside: false
editLink: false
---
+2 -2
View File
@@ -45,8 +45,8 @@
"element-to-path": "^1.2.1",
"fuse.js": "^7.1.0",
"jszip": "^3.10.1",
"lodash": "^4.17.23",
"lodash-es": "^4.17.23",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lucide-react": "workspace:*",
"lucide-vue-next": "workspace:*",
"react": "^18.3.1",
-1
View File
@@ -1,5 +1,4 @@
---
title: Packages
layout: page
outline: 2
outlineTitle: Packages
@@ -1 +0,0 @@
<svg width="161" height="132" viewBox="0 0 161 132" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="0%" y1="50%" y2="50%" id="a"><stop stop-color="#2A3B8F" offset="0%"/><stop stop-color="#29ABE2" offset="100%"/></linearGradient><linearGradient x1="100%" y1="50%" x2="0%" y2="50%" id="c"><stop stop-color="#B4D44E" offset="0%"/><stop stop-color="#E7F716" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="#166DA5" d="M160.6 65.9l-17.4 29.3-24.4-29.7 24.4-28.9z"/><path fill="#8FDB69" d="M141.3 100.2l-26.5-31.7-15.9 26.6 24.7 36.1z"/><path fill="#166DA5" d="M141 31.4l-26.2 31.8-15.9-26.6L123.6.9z"/><path fill="url(#a)" opacity=".95" d="M61.1 31.4H141L123.4.7H78.7z M114.8 63.3H159l-15.9-26.8H98.8"/><path fill="url(#c)" opacity=".95" d="M141.3 100.3H61l17.6 30.5h45z M114.8 68.4H159l-15.9 26.8H98.8"/><path fill="#010101" d="M78.6 130.8L41 65.8 79.1.8H37.9L.4 65.8l37.5 65z"/></g></svg>

Before

Width:  |  Height:  |  Size: 936 B

-7
View File
@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="138" height="42" fill="none" viewBox="0 0 138 42">
<path fill="#fff" fill-rule="evenodd" d="M138 4a3.997 3.997 0 0 0-4-4H4a4 4 0 0 0-4 4v34a4 4 0 0 0 4 4h130a4 4 0 0 0 4-4z" clip-rule="evenodd"/>
<path fill="#000" fill-rule="evenodd" d="M27.963 16.943q1.08 0 1.864.43.783.428 1.212 1.154v-1.454h3.653V27.53H31.04v-1.454q-.429.727-1.23 1.156-.783.43-1.846.43-1.248 0-2.274-.653-1.006-.653-1.603-1.864-.578-1.23-.578-2.852t.578-2.834q.597-1.211 1.604-1.863a4.15 4.15 0 0 1 2.273-.653m1.175 3.188q-.82 0-1.36.577-.542.56-.542 1.585 0 1.006.541 1.602.54.579 1.36.579.802 0 1.343-.579.558-.578.56-1.602-.001-1.007-.56-1.585a1.76 1.76 0 0 0-1.342-.577" clip-rule="evenodd"/>
<path fill="#000" d="M75.361 16.943q2.05 0 3.225 1.006t1.361 2.648h-3.3q-.092-.504-.447-.765-.335-.28-.913-.28-.447 0-.69.169a.55.55 0 0 0-.223.465q0 .373.429.579.428.186 1.379.39 1.137.243 1.882.541.765.3 1.324.969.578.653.578 1.79 0 .914-.541 1.64-.522.727-1.51 1.156-.969.41-2.293.41-1.417 0-2.535-.466-1.1-.485-1.752-1.304a3.47 3.47 0 0 1-.708-1.883h3.542q.075.503.466.782.39.28.968.28.448 0 .71-.186a.57.57 0 0 0 .26-.485q0-.39-.428-.578-.43-.185-1.417-.41-1.12-.223-1.865-.503a3.25 3.25 0 0 1-1.304-.914q-.54-.633-.54-1.715 0-.93.502-1.677.504-.765 1.473-1.211.989-.448 2.367-.448"/>
<path fill="#000" fill-rule="evenodd" d="M94.305 16.943q1.564 0 2.796.653a4.8 4.8 0 0 1 1.957 1.863q.726 1.212.727 2.834 0 1.621-.727 2.852a4.8 4.8 0 0 1-1.957 1.864q-1.25.652-2.815.652-1.566 0-2.814-.652a4.8 4.8 0 0 1-1.939-1.864q-.709-1.212-.709-2.852 0-1.622.71-2.834a4.95 4.95 0 0 1 1.956-1.863q1.248-.653 2.815-.653m0 3.168q-.746 0-1.25.56-.502.56-.502 1.622 0 1.08.484 1.64.485.559 1.25.56.763-.001 1.267-.56.52-.578.521-1.64 0-1.063-.521-1.622a1.61 1.61 0 0 0-1.25-.56M118.921 16.943q1.081 0 1.864.43.783.428 1.212 1.154v-1.454h3.653V27.53h-3.653v-1.454q-.429.727-1.23 1.156-.784.43-1.846.43-1.248 0-2.274-.653-1.006-.653-1.603-1.864-.577-1.23-.577-2.852t.577-2.834q.597-1.211 1.603-1.863a4.15 4.15 0 0 1 2.274-.653m1.175 3.188q-.82 0-1.361.577-.54.56-.541 1.585 0 1.006.541 1.602.541.579 1.361.579.8 0 1.342-.579.558-.578.559-1.602 0-1.007-.559-1.585a1.77 1.77 0 0 0-1.342-.577M17.81 14.39q1.584 0 2.683.558 1.119.56 1.678 1.548.56.988.56 2.274 0 1.193-.56 2.18-.54.97-1.66 1.567-1.099.577-2.701.577H16v4.436h-3.653V14.39zM16 20.186h1.454q1.548 0 1.547-1.416 0-1.436-1.547-1.436h-1.454z" clip-rule="evenodd"/>
<path fill="#000" d="m41.658 19.702 1.51-2.629h4.008l-3.56 5.331 3.69 5.126h-4.1l-1.884-2.628-1.49 2.628h-4.009l3.487-5.424-3.635-5.033h4.1zM55.42 19.33h4.456v-4.94h3.653v13.14h-3.653v-5.274h-4.455v5.274h-3.653V14.39h3.653zM69.18 27.53h-3.654V17.073h3.654zM85.919 17.073h1.9v3.038h-1.9v3.505q0 .429.186.615.204.187.671.187h1.063v3.112h-1.585q-4.008 0-4.008-3.97v-3.449H80.96v-3.038h1.286v-2.535h3.673zM108.205 20.876h-1.024q-1.194 0-1.772.466-.577.465-.577 1.622v4.566h-3.654V17.073h3.654v1.846a4.76 4.76 0 0 1 1.491-1.417 3.6 3.6 0 0 1 1.882-.521zM113.075 27.53h-3.653V17.073h3.653zM67.372 12.339q.95 0 1.528.54.597.542.597 1.361 0 .801-.597 1.342-.577.523-1.528.523-.97 0-1.565-.523a1.76 1.76 0 0 1-.579-1.342q0-.82.579-1.36.596-.54 1.565-.541M111.268 12.339q.95 0 1.528.54.596.542.597 1.361 0 .801-.597 1.342-.578.522-1.528.523-.97 0-1.567-.523a1.76 1.76 0 0 1-.577-1.342q0-.82.577-1.36.597-.54 1.567-.541"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

-1
View File
@@ -1,5 +1,4 @@
---
title: Showcase
layout: page
outline: 2
outlineTitle: Showcase
+1 -2
View File
@@ -1,8 +1,7 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley",
"jguddas"
"danielbayley"
],
"tags": [
"read",
+1 -1
View File
@@ -9,6 +9,6 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z" />
<path d="m9 10 2 2 4-4" />
</svg>

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 301 B

+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "../icon.schema.json",
"contributors": [
"siarie",
"ericfennis",
"jguddas"
"ericfennis"
],
"tags": [
"delete",
+2 -2
View File
@@ -9,6 +9,6 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 10H9" />
<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
<line x1="15" x2="9" y1="10" y2="10" />
</svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 317 B

+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "../icon.schema.json",
"contributors": [
"siarie",
"ericfennis",
"jguddas"
"ericfennis"
],
"tags": [
"add"
+3 -3
View File
@@ -9,7 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 7v6" />
<path d="M15 10H9" />
<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
<line x1="12" x2="12" y1="7" y2="13" />
<line x1="15" x2="9" y1="10" y2="10" />
</svg>

Before

Width:  |  Height:  |  Size: 389 B

After

Width:  |  Height:  |  Size: 359 B

+1 -2
View File
@@ -1,8 +1,7 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley",
"jguddas"
"danielbayley"
],
"tags": [
"read",
+1 -1
View File
@@ -9,7 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z" />
<path d="m14.5 7.5-5 5" />
<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
<path d="m9.5 7.5 5 5" />
</svg>

Before

Width:  |  Height:  |  Size: 399 B

After

Width:  |  Height:  |  Size: 329 B

+1 -10
View File
@@ -4,18 +4,9 @@
"colebemis",
"csandman",
"siarie",
"ericfennis",
"jguddas"
"ericfennis"
],
"tags": [
"save",
"favorite",
"mark",
"label",
"attachment",
"file",
"stick",
"pin",
"read",
"clip",
"marker",
+1 -1
View File
@@ -9,5 +9,5 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
</svg>

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 275 B

+2 -2
View File
@@ -9,7 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10.94 5.274A7 7 0 0 1 15.71 10h1.79a4.5 4.5 0 0 1 4.222 6.057" />
<path d="M18.796 18.81A4.5 4.5 0 0 1 17.5 19H9A7 7 0 0 1 5.79 5.78" />
<path d="m2 2 20 20" />
<path d="M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193" />
<path d="M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07" />
</svg>

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 388 B

+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "../icon.schema.json",
"contributors": [
"ericfennis",
"karsa-mistmere",
"jguddas"
"karsa-mistmere"
],
"tags": [
"weather",
+2 -2
View File
@@ -9,6 +9,6 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17.5 12a1 1 0 1 1 0 9H9.006a7 7 0 1 1 6.702-9z" />
<path d="M21.832 9A3 3 0 0 0 19 7h-2.207a5.5 5.5 0 0 0-10.72.61" />
<path d="M17.5 21H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
<path d="M22 10a3 3 0 0 0-3-3h-2.207a5.502 5.502 0 0 0-10.702.5" />
</svg>

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 345 B

-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"karsa-mistmere",
"Muhammad-Aqib-Bashir"
],
"tags": [
"globe",
"internet",
"offline",
"disconnected",
"network",
"connection",
"world",
"no connection",
"network failure",
"signal off"
],
"categories": [
"connectivity",
"devices",
"navigation"
]
}
-15
View File
@@ -1,15 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 3 5 5" />
<path d="M2 12h20A10 10 0 1 1 12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 4-10" />
<path d="m21 3-5 5" />
</svg>

Before

Width:  |  Height:  |  Size: 341 B

+1 -1
View File
@@ -9,7 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3.704 14.467a10 8 0 1 1 3.115 2.375" />
<path d="M3.704 14.467A10 8 0 0 1 2 10a10 8 0 0 1 20 0 10 8 0 0 1-10 8 10 8 0 0 1-5.181-1.158" />
<path d="M7 22a5 5 0 0 1-2-3.994" />
<circle cx="5" cy="16" r="2" />
</svg>

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 381 B

+13 -10
View File
@@ -9,14 +9,17 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 3h2" />
<path d="M16 19h-2" />
<path d="M2 12v-2" />
<path d="M2 16v5.286a.71.71 0 0 0 1.212.502l1.149-1.149" />
<path d="M20 19a2 2 0 0 0 2-2v-1" />
<path d="M22 10v2" />
<path d="M22 6V5a2 2 0 0 0-2-2" />
<path d="M4 3a2 2 0 0 0-2 2v1" />
<path d="M8 19h2" />
<path d="M8 3h2" />
<path d="M12 19h.01" />
<path d="M12 3h.01" />
<path d="M16 19h.01" />
<path d="M16 3h.01" />
<path d="M2 13h.01" />
<path d="M2 17v4.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H8" />
<path d="M2 5a2 2 0 0 1 2-2" />
<path d="M2 9h.01" />
<path d="M20 3a2 2 0 0 1 2 2" />
<path d="M22 13h.01" />
<path d="M22 17a2 2 0 0 1-2 2" />
<path d="M22 9h.01" />
<path d="M8 3h.01" />
</svg>

Before

Width:  |  Height:  |  Size: 523 B

After

Width:  |  Height:  |  Size: 622 B

+4 -4
View File
@@ -9,9 +9,9 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 17v4" />
<path d="M17 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 1.184-1.826" />
<path d="m2 2 20 20" />
<path d="M17 17H4a2 2 0 0 1-2-2V5c0-1.5 1-2 1-2" />
<path d="M22 15V5a2 2 0 0 0-2-2H9" />
<path d="M8 21h8" />
<path d="M8.656 3H20a2 2 0 0 1 2 2v10a2 2 0 0 1-.293 1.042" />
<path d="M12 17v4" />
<path d="m2 2 20 20" />
</svg>

Before

Width:  |  Height:  |  Size: 408 B

After

Width:  |  Height:  |  Size: 375 B

-22
View File
@@ -1,22 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"colebemis",
"csandman",
"ericfennis",
"jguddas",
"lt25106"
],
"tags": [
"fax",
"office",
"device",
"cross",
"cancel",
"remove",
"error"
],
"categories": [
"devices"
]
}
-17
View File
@@ -1,17 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12.531 22H7a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h6.377" />
<path d="m16.5 16.5 5 5" />
<path d="m16.5 21.5 5-5" />
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.5" />
<path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6" />
</svg>

Before

Width:  |  Height:  |  Size: 463 B

+3 -3
View File
@@ -9,7 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m10.344 4.688 1.181-2.393a.53.53 0 0 1 .95 0l2.31 4.679a2.12 2.12 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.237 3.152" />
<path d="m17.945 17.945.43 2.505a.53.53 0 0 1-.771.56l-4.618-2.428a2.12 2.12 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a8 8 0 0 0 .4-.099" />
<path d="m2 2 20 20" />
<path d="M8.34 8.34 2 9.27l5 4.87L5.82 21 12 17.77 18.18 21l-.59-3.43" />
<path d="M18.42 12.76 22 9.27l-6.91-1L12 2l-1.44 2.91" />
<line x1="2" x2="22" y1="2" y2="22" />
</svg>

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 385 B

+1 -1
View File
@@ -10,7 +10,7 @@
stroke-linejoin="round"
>
<path d="M10.5 17h1.227a2 2 0 0 0 1.345-.52L18 12" />
<path d="m12 13.5 3.794.506" />
<path d="m12 13.5 3.75.5" />
<path d="m3.173 8.18 11-5a2 2 0 0 1 2.647.993L18.56 8" />
<path d="M6 10V8" />
<path d="M6 14v1" />

Before

Width:  |  Height:  |  Size: 480 B

After

Width:  |  Height:  |  Size: 477 B

+1 -2
View File
@@ -1,8 +1,7 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley",
"karsa-mistmere"
"danielbayley"
],
"tags": [
"indirection",
+7 -7
View File
@@ -9,11 +9,11 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m10.586 5.414-5.172 5.172" />
<path d="m18.586 13.414-5.172 5.172" />
<path d="M6 12h12" />
<circle cx="12" cy="20" r="2" />
<circle cx="12" cy="4" r="2" />
<circle cx="20" cy="12" r="2" />
<circle cx="4" cy="12" r="2" />
<circle cx="12" cy="4.5" r="2.5" />
<path d="m10.2 6.3-3.9 3.9" />
<circle cx="4.5" cy="12" r="2.5" />
<path d="M7 12h10" />
<circle cx="19.5" cy="12" r="2.5" />
<path d="m13.8 17.7 3.9-3.9" />
<circle cx="12" cy="19.5" r="2.5" />
</svg>

Before

Width:  |  Height:  |  Size: 453 B

After

Width:  |  Height:  |  Size: 453 B

-1
View File
@@ -21,7 +21,6 @@
"addjsons": "node ./scripts/addMissingIconJsonFiles.mts",
"checkIcons": "node ./scripts/checkIconsAndCategories.mts",
"generate:changelog": "node ./scripts/generateChangelog.mts",
"generate:backers": "node scripts/updateOpenCollectiveBackers.mts",
"generate:contributors": "node ./scripts/updateContributors.mts icons/*.svg",
"generate:nextJSAliases": "node ./scripts/generateNextJSAliases.mts",
"suggest:tags": "node ./scripts/suggestTags.mts",
+38
View File
@@ -0,0 +1,38 @@
module.exports = {
root: true,
overrides: [
{
files: ['*.ts'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@angular-eslint/recommended',
'plugin:@angular-eslint/template/process-inline-templates',
'prettier',
],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lucide',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'attribute',
prefix: ['lucide'],
style: 'camelCase',
},
],
},
},
{
files: ['*.html'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {},
},
],
};
+73
View File
@@ -0,0 +1,73 @@
<p align="center">
<a href="https://github.com/lucide-icons/lucide">
<img src="https://lucide.dev/package-logos/lucide-angular.svg" alt="Lucide icon library for Angular applications." width="540">
</a>
</p>
<p align="center">
Lucide icon library for Angular applications.
</p>
<div align="center">
[![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-angular)
[![GitHub](https://img.shields.io/github/license/lucide-icons/lucide)](https://lucide.dev/license)
</div>
<p align="center">
<a href="https://lucide.dev/guide/">About</a>
·
<a href="https://lucide.dev/icons/">Icons</a>
·
<a href="https://lucide.dev/guide/packages/lucide-angular">Documentation</a>
·
<a href="https://lucide.dev/license">License</a>
</p>
# Lucide Angular
Implementation of the lucide icon library for angular applications.
## Installation
```sh
pnpm add lucide-angular
```
```sh
npm install lucide-angular
```
```sh
yarn add lucide-angular
```
```sh
bun add lucide-angular
```
## Documentation
For full documentation, visit [lucide.dev](https://lucide.dev/guide/packages/lucide-angular)
## Community
Join the [Discord server](https://discord.gg/EH6nSts) to chat with the maintainers and other users.
## License
Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/license).
## Sponsors
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
<img src="https://lucide.dev/vercel.svg" alt="Powered by Vercel" width="200" />
</a>
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
### Awesome backers 🍺
<a href="https://www.scipress.io?utm_source=lucide"><img src="https://lucide.dev/sponsors/scipress.svg" width="180" alt="Scipress sponsor badge" /></a>
<a href="https://github.com/pdfme/pdfme"><img src="https://lucide.dev/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a>
+51
View File
@@ -0,0 +1,51 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"@lucide/angular": {
"projectType": "library",
"root": ".",
"sourceRoot": "./src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"configurations": {
"production": {
"tsConfig": "./tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "./tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "./tsconfig.spec.json",
"coverage": true,
"coverageReporters": ["html", "lcov"],
"coverageExclude": ["src/icons/*"],
"coverageThresholds": {
"statements": 80,
"branches": 80,
"functions": 80,
"lines": 80
}
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
}
}
}
},
"cli": {
"packageManager": "pnpm"
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/ng-packagr/ng-package.schema.json",
"dest": "./dist",
"lib": {
"entryFile": "./src/public-api.ts"
}
}
+75
View File
@@ -0,0 +1,75 @@
{
"name": "@lucide/angular",
"description": "A Lucide icon library package for Angular applications.",
"version": "0.0.1",
"author": "SMAH1",
"license": "ISC",
"homepage": "https://lucide.dev",
"bugs": "https://github.com/lucide-icons/lucide/issues",
"repository": {
"type": "git",
"url": "https://github.com/lucide-icons/lucide.git",
"directory": "packages/lucide-angular"
},
"publishConfig": {
"directory": "dist"
},
"scripts": {
"ng": "ng",
"watch": "ng build --watch --configuration development",
"prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons",
"build": "pnpm build:ng",
"copy:license": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist && rm -rf ./src/icons/*.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts",
"build:ng": "ng build --configuration production",
"test": "ng test --no-watch",
"test:watch": "ng test",
"lint": "npx eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
"e2e": "ng e2e",
"version": "pnpm version --git-tag-version=false"
},
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true,
"devDependencies": {
"@angular-eslint/builder": "~21.1.0",
"@angular-eslint/eslint-plugin": "~21.1.0",
"@angular-eslint/eslint-plugin-template": "~21.1.0",
"@angular-eslint/schematics": "~21.1.0",
"@angular-eslint/template-parser": "~21.1.0",
"@angular/build": "^21.0.3",
"@angular/cli": "^21.0.3",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/compiler-cli": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0",
"@lucide/build-icons": "workspace:*",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "^4.0.16",
"angular-eslint": "21.1.0",
"jsdom": "^27.1.0",
"ng-packagr": "^21.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"typescript": "~5.9.2",
"vitest": "^4.0.16"
},
"peerDependencies": {
"@angular/common": "13.x - 21.x",
"@angular/core": "13.x - 21.x"
}
}
@@ -0,0 +1,68 @@
import base64SVG from '@lucide/build-icons/utils/base64SVG';
import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate';
export default defineExportTemplate(async ({
componentName,
iconName,
children,
getSvg,
deprecated,
deprecationReason,
aliases = [],
toPascalCase,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
const angularComponentName = `Lucide${componentName}`;
const selectors = [`svg[lucide${toPascalCase(iconName)}]`];
const aliasComponentNames: string[] = [];
for (const alias of aliases) {
const aliasName = typeof alias === 'string' ? alias : alias.name;
const aliasComponentName = `Lucide${toPascalCase(aliasName)}`;
const aliasSelector = `svg[lucide${toPascalCase(aliasName)}]`;
if (!selectors.includes(aliasSelector)) {
selectors.push(aliasSelector);
}
if (aliasComponentName !== angularComponentName && !aliasComponentNames.includes(aliasComponentName)) {
aliasComponentNames.push(aliasComponentName);
}
}
return `\
import { LucideIconData } from '../types';
import { LucideIconBase } from '../lucide-icon-base';
import { Component, signal } from '@angular/core';
/**
* @component @name ${componentName}
* @description Lucide SVG icon component, renders SVG Element with children.
*
* @preview ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName}
* @see https://lucide.dev/guide/packages/lucide-angular - Documentation
*
* @param {Object} props - Lucide icons props and any valid SVG attribute
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
@Component({
selector: '${selectors.join(', ')}',
templateUrl: '../lucide-icon.html',
standalone: true,
})
export class ${angularComponentName} extends LucideIconBase {
static iconName = '${iconName}';
static iconData: LucideIconData = ${JSON.stringify(children)};
override readonly iconName = signal(${angularComponentName}.iconName);
override readonly iconData = signal(${angularComponentName}.iconData);
}
${aliasComponentNames.map((aliasComponentName) => {
return `
/**
* @deprecated
* @see ${angularComponentName}
*/
export const ${aliasComponentName} = ${angularComponentName};
`;
}).join(`\n\n`)}
`;
});
@@ -0,0 +1,11 @@
export default {
xmlns: 'http://www.w3.org/2000/svg',
width: '24',
height: '24',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
};
@@ -0,0 +1,25 @@
import { TestBed } from '@angular/core/testing';
import { LUCIDE_CONFIG, lucideDefaultConfig, provideLucideConfig } from './lucide-config';
describe('Lucide config', () => {
describe('LUCIDE_CONFIG', () => {
it('should use default', () => {
expect(TestBed.inject(LUCIDE_CONFIG)).toBe(lucideDefaultConfig);
});
});
describe('provideLucideConfig', () => {
it('should use defaults', () => {
TestBed.configureTestingModule({
providers: [
provideLucideConfig({
size: 18,
}),
],
});
expect(TestBed.inject(LUCIDE_CONFIG)).toEqual({
...lucideDefaultConfig,
size: 18,
});
});
});
});
+38
View File
@@ -0,0 +1,38 @@
import { InjectionToken, Provider } from '@angular/core';
/**
* A configuration service for Lucide icon components.
*
* You can inject this service, typically in AppComponent, and customize its property values in
* order to provide default values for all the icons used in the application.
*/
export interface LucideConfig {
color: string;
size: number;
strokeWidth: number;
absoluteStrokeWidth: boolean;
}
export const lucideDefaultConfig: LucideConfig = {
color: 'currentColor',
size: 24,
strokeWidth: 2,
absoluteStrokeWidth: false,
};
export const LUCIDE_CONFIG = new InjectionToken<LucideConfig>(
'Lucide icon config',
{
factory: () => lucideDefaultConfig,
},
);
export function provideLucideConfig(config: Partial<LucideConfig>): Provider {
return {
provide: LUCIDE_CONFIG,
useValue: {
...lucideDefaultConfig,
...config,
},
};
}
+114
View File
@@ -0,0 +1,114 @@
import {
Component,
computed,
effect,
ElementRef,
inject,
input,
Renderer2,
Signal,
} from '@angular/core';
import { LUCIDE_CONFIG } from './lucide-config';
import { LucideIconData, Nullable } from './types';
import defaultAttributes from './default-attributes';
import { formatFixed } from './utils/format-fixed';
import { toKebabCase } from './utils/to-kebab-case';
function transformNumericStringInput(
value: Nullable<string | number>,
defaultValue: number,
): number {
if (typeof value === 'string') {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
return defaultValue;
}
return parsedValue;
}
return value ?? defaultValue;
}
/**
* @internal
*/
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'svg[lucideIcon]',
templateUrl: './lucide-icon.html',
host: {
...defaultAttributes,
class: 'lucide',
'[attr.width]': 'size().toString(10)',
'[attr.height]': 'size().toString(10)',
'[attr.stroke]': 'color()',
'[attr.stroke-width]': 'computedStrokeWidth()',
'[attr.aria-hidden]': 'ariaHidden()',
},
})
export abstract class LucideIconBase {
abstract iconName: Signal<Nullable<string>>;
abstract iconData: Signal<Nullable<LucideIconData>>;
protected readonly iconConfig = inject(LUCIDE_CONFIG);
protected readonly elRef = inject(ElementRef);
protected readonly renderer = inject(Renderer2);
readonly title = input<Nullable<string>>();
readonly ariaHidden = computed(() => {
return !this.title();
});
readonly size = input(this.iconConfig.size, {
transform: (value: Nullable<string | number>) =>
transformNumericStringInput(value, this.iconConfig.size),
});
readonly color = input(this.iconConfig.color, {
transform: (value: Nullable<string>) => value ?? this.iconConfig.color,
});
readonly strokeWidth = input(this.iconConfig.strokeWidth, {
transform: (value: Nullable<string | number>) =>
transformNumericStringInput(value, this.iconConfig.strokeWidth),
});
readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, {
transform: (value: Nullable<boolean>) => value ?? this.iconConfig.absoluteStrokeWidth,
});
protected readonly computedStrokeWidth = computed(() => {
const strokeWidth = this.strokeWidth();
const size = this.size();
return this.absoluteStrokeWidth()
? formatFixed(strokeWidth / (size / 24))
: strokeWidth.toString(10);
});
constructor() {
effect((onCleanup) => {
const icon = this.iconData();
if (icon) {
const elements = icon.map(([name, attrs]) => {
const element = this.renderer.createElement(name, 'http://www.w3.org/2000/svg');
for (const [name, value] of Object.entries(attrs)) {
this.renderer.setAttribute(
element,
name,
typeof value === 'number' ? value.toString(10) : value,
);
}
this.renderer.appendChild(this.elRef.nativeElement, element);
return element;
});
onCleanup(() => {
for (const element of elements) {
this.renderer.removeChild(this.elRef.nativeElement, element);
}
});
}
});
effect((onCleanup) => {
const name = this.iconName();
if (name) {
const cssClass = `lucide-${toKebabCase(name)}`;
this.renderer.addClass(this.elRef.nativeElement, cssClass);
onCleanup(() => {
this.renderer.removeClass(this.elRef.nativeElement, cssClass);
});
}
});
}
}
+4
View File
@@ -0,0 +1,4 @@
@if (title(); as titleValue) {
<title>{{ titleValue }}</title>
}
<ng-content />
+243
View File
@@ -0,0 +1,243 @@
import { Component, input, inputBinding, signal, WritableSignal } from '@angular/core';
import { LucideIcon } from './lucide-icon';
import { LucideIconData, LucideIconInput } from './types';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideLucideIcons } from './lucide-icons';
import { LucideActivity } from './icons/activity';
import { By } from '@angular/platform-browser';
@Component({
template: `@if (icon(); as iconData) {
<svg [lucideIcon]="iconData">
<rect x="1" y="1" width="22" height="22" />
</svg>
}`,
imports: [LucideIcon],
})
class TestHostComponent {
readonly icon = input<LucideIconData>();
}
describe('LucideIcon', () => {
let component: LucideIcon;
let fixture: ComponentFixture<LucideIcon>;
let icon: WritableSignal<LucideIconInput | null | undefined>;
let name: WritableSignal<string | undefined>;
let title: WritableSignal<string | undefined>;
let color: WritableSignal<string | undefined>;
let size: WritableSignal<string | number | undefined>;
let strokeWidth: WritableSignal<string | number | undefined>;
let absoluteStrokeWidth: WritableSignal<boolean | undefined>;
const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr);
const testIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
const testIcon2: LucideIconData = [
['circle', { cx: 12, cy: 12, r: 8 }],
['polyline', { points: '1 1 22 22' }],
];
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [provideLucideIcons({ demo: testIcon })],
});
icon = signal('demo');
name = signal(undefined);
title = signal(undefined);
color = signal(undefined);
size = signal(undefined);
strokeWidth = signal(undefined);
absoluteStrokeWidth = signal(undefined);
fixture = TestBed.createComponent(LucideIcon, {
inferTagName: true,
bindings: [
inputBinding('lucideIcon', icon),
inputBinding('name', name),
inputBinding('title', title),
inputBinding('color', color),
inputBinding('size', size),
inputBinding('strokeWidth', strokeWidth),
inputBinding('absoluteStrokeWidth', absoluteStrokeWidth),
],
});
component = fixture.componentInstance;
});
it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should render children', () => {
icon.set(testIcon2);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toBe(
'<!--container--><circle cx="12" cy="12" r="8"></circle><polyline points="1 1 22 22"></polyline>',
);
});
it('should remove children on change', () => {
icon.set(null);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toBe('<!--container-->');
});
describe('iconInput', () => {
it('should support LucideIconData input', () => {
icon.set(testIcon);
name.set('custom-name');
fixture.detectChanges();
expect(component.iconData()).toBe(testIcon);
expect(component.iconName()).toBe('custom-name');
expect(fixture.nativeElement.innerHTML).toBe(
'<!--container--><polyline points="1 1 22 22"></polyline>',
);
});
it('should support LucideIconComponentType input', () => {
icon.set(LucideActivity);
fixture.detectChanges();
expect(component.iconData()).toBe(LucideActivity.iconData);
expect(component.iconName()).toBe(LucideActivity.iconName);
});
it('should support string icon name', () => {
icon.set('demo');
fixture.detectChanges();
expect(component.iconData()).toBe(testIcon);
expect(component.iconName()).toBe('demo');
});
it('should throw error if no icon founds', () => {
icon.set('invalid');
expect(() => fixture.detectChanges()).toThrowError(`Unable to resolve icon 'invalid'`);
});
});
describe('class', () => {
it('should add all classes', () => {
fixture.detectChanges();
expect(getSvgAttribute('class')).toBe('lucide lucide-demo');
});
it('should add class from name, even if icon has name', () => {
icon.set(LucideActivity);
name.set('custom-name');
fixture.detectChanges();
expect(getSvgAttribute('class')).toBe('lucide lucide-custom-name');
});
it('should add class icon if available', () => {
icon.set(LucideActivity);
fixture.detectChanges();
expect(getSvgAttribute('class')).toBe('lucide lucide-activity');
});
it('should remove class on change', () => {
icon.set(null);
fixture.detectChanges();
expect(getSvgAttribute('class')).toBe('lucide');
});
});
describe('color', () => {
it('should default to currentColor', () => {
fixture.detectChanges();
expect(getSvgAttribute('stroke')).toBe('currentColor');
});
it('should set color', () => {
color.set('red');
fixture.detectChanges();
expect(getSvgAttribute('stroke')).toBe('red');
});
});
describe('size', () => {
it('should default to 24', () => {
fixture.detectChanges();
expect(getSvgAttribute('width')).toBe('24');
expect(getSvgAttribute('height')).toBe('24');
});
it('should set size', () => {
size.set(12);
fixture.detectChanges();
expect(getSvgAttribute('width')).toBe('12');
expect(getSvgAttribute('height')).toBe('12');
});
it('should allow string size', () => {
size.set('18');
fixture.detectChanges();
expect(getSvgAttribute('width')).toBe('18');
expect(getSvgAttribute('height')).toBe('18');
});
it('should use default on invalid string', () => {
size.set('large');
fixture.detectChanges();
expect(getSvgAttribute('width')).toBe('24');
expect(getSvgAttribute('height')).toBe('24');
});
});
describe('strokeWidth', () => {
it('should default to 2', () => {
fixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe('2');
});
it('should set stroke width', () => {
strokeWidth.set(1.41);
fixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe('1.41');
});
it('should allow string stroke width', () => {
strokeWidth.set('1px');
fixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe('1');
});
});
describe('absoluteStrokeWidth', () => {
it('should not adjust stroke width', () => {
strokeWidth.set(2);
size.set(12);
absoluteStrokeWidth.set(false);
fixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe('2');
});
it('should adjust stroke width', () => {
strokeWidth.set(2);
size.set(12);
absoluteStrokeWidth.set(true);
fixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe('4');
});
});
describe('title', () => {
it('should set title if provided', () => {
title.set('Foobar');
fixture.detectChanges();
const titleEl = fixture.debugElement.query(By.css('title')).nativeElement;
expect(titleEl).toBeDefined();
expect(titleEl.textContent).toBe('Foobar');
});
it('should not set aria-hidden when title is set', () => {
title.set('Foobar');
fixture.detectChanges();
expect(getSvgAttribute('aria-hidden')).toBeUndefined;
});
it('should set aria-hidden if no title is provided', () => {
title.set(undefined);
fixture.detectChanges();
expect(getSvgAttribute('aria-hidden')).toBeUndefined;
});
});
describe('content projection', () => {
it('should project content', () => {
const hostFixture = TestBed.createComponent(TestHostComponent);
hostFixture.componentRef.setInput('icon', testIcon);
hostFixture.detectChanges();
hostFixture.componentRef.setInput('icon', testIcon2);
hostFixture.detectChanges();
const rect = hostFixture.debugElement.query(By.css('rect')).nativeElement;
expect(rect).toBeInstanceOf(SVGElement);
expect(rect.outerHTML).toBe('<rect x="1" y="1" width="22" height="22"></rect>');
});
});
});
+62
View File
@@ -0,0 +1,62 @@
import { Component, computed, inject, input } from '@angular/core';
import { isLucideIconComponent, isLucideIconData, LucideIconInput } from './types';
import { LucideIconBase } from './lucide-icon-base';
import { LUCIDE_ICONS } from './lucide-icons';
import { LucideIconData } from './types';
import { toKebabCase } from './utils/to-kebab-case';
interface LucideResolvedIcon {
name?: string | null;
data: LucideIconData;
}
@Component({
selector: 'svg[lucideIcon]',
templateUrl: './lucide-icon.html',
standalone: true,
})
export class LucideIcon extends LucideIconBase {
protected readonly icons = inject(LUCIDE_ICONS);
readonly name = input<string | null>();
readonly iconInput = input.required<LucideIconInput | null>({
alias: 'lucideIcon',
});
readonly resolvedIcon = computed<LucideResolvedIcon | null>(() => {
return this.resolveIcon(this.name(), this.iconInput());
});
override readonly iconName = computed(() => {
return this.resolvedIcon()?.name;
});
override readonly iconData = computed(() => {
return this.resolvedIcon()?.data;
});
protected resolveIcon(
name: string | null | undefined,
icon: LucideIconInput | null | undefined,
): LucideResolvedIcon | null {
if (isLucideIconData(icon)) {
return {
name,
data: icon,
};
} else if (isLucideIconComponent(icon)) {
return {
name: name ?? icon.iconName,
data: icon.iconData,
};
} else if (typeof icon === 'string') {
const name = toKebabCase(icon);
if (name in this.icons) {
return {
name,
data: this.icons[name],
};
} else {
throw new Error(`Unable to resolve icon '${icon}'`);
}
}
return null;
}
}
+44
View File
@@ -0,0 +1,44 @@
import { TestBed } from '@angular/core/testing';
import { LUCIDE_ICONS, provideLucideIcons } from './lucide-icons';
import { LucideIconData } from './types';
import { LucideActivity } from './icons/activity';
import { LucideCircle } from './icons/circle';
import { LucideSquareX } from './icons/square-x';
describe('Lucide icons', () => {
describe('LUCIDE_ICONS', () => {
it('should default to empty map', () => {
expect(TestBed.inject(LUCIDE_ICONS)).toEqual({});
});
});
describe('provideLucideIcons', () => {
const mockIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
const mockIcon2: LucideIconData = [['circle', { cx: 12, cy: 12, r: 8 }]];
it('should accept dictionary of icons', () => {
TestBed.configureTestingModule({
providers: [
provideLucideIcons({
DemoIcon: mockIcon,
MockIcon: mockIcon2,
TestIcon: LucideActivity,
}),
],
});
expect(TestBed.inject(LUCIDE_ICONS)).toEqual({
'demo-icon': mockIcon,
'mock-icon': mockIcon2,
[LucideActivity.iconName]: LucideActivity.iconData,
});
});
it('should accept list of icon components', () => {
TestBed.configureTestingModule({
providers: [provideLucideIcons([LucideActivity, LucideSquareX, LucideCircle])],
});
expect(TestBed.inject(LUCIDE_ICONS)).toEqual({
[LucideActivity.iconName]: LucideActivity.iconData,
[LucideSquareX.iconName]: LucideSquareX.iconData,
[LucideCircle.iconName]: LucideCircle.iconData,
});
});
});
});
+34
View File
@@ -0,0 +1,34 @@
import { InjectionToken, Provider } from '@angular/core';
import { LucideIconData, LucideIcons } from './types';
import { isLucideIconComponent, LucideIconComponentType } from './types';
import { toKebabCase } from './utils/to-kebab-case';
export const LUCIDE_ICONS = new InjectionToken<LucideIcons>('Lucide icons', {
factory: () => ({}),
});
export function provideLucideIcons(
icons: Record<string, LucideIconData | LucideIconComponentType> | Array<LucideIconComponentType>,
): Provider {
if (Array.isArray(icons)) {
return {
provide: LUCIDE_ICONS,
useValue: icons.reduce((acc, icon) => {
acc[toKebabCase(icon.iconName)] = icon.iconData;
return acc;
}, {} as LucideIcons),
};
} else {
return {
provide: LUCIDE_ICONS,
useValue: Object.entries(icons).reduce((acc, [name, icon]) => {
if (isLucideIconComponent(icon)) {
acc[icon.iconName] = icon.iconData;
} else {
acc[toKebabCase(name)] = icon;
}
return acc;
}, {} as LucideIcons),
};
}
}
+8
View File
@@ -0,0 +1,8 @@
import * as icons from './icons/lucide-angular';
export * from './lucide-config';
export * from './lucide-icon';
export * from './lucide-icons';
export * from './types';
export * from './icons/lucide-angular';
export { icons };
+34
View File
@@ -0,0 +1,34 @@
import { Signal, Type } from '@angular/core';
type HtmlAttributes = { [key: string]: string | number };
export type LucideIconNode = readonly [string, HtmlAttributes];
export type LucideIconData = readonly LucideIconNode[];
export type LucideIcons = { [key: string]: LucideIconData };
export interface LucideIconComponentInterface {
iconName: Signal<Nullable<string>>;
iconData: Signal<Nullable<LucideIconData>>;
}
export type LucideIconComponentType = Type<LucideIconComponentInterface> & {
iconName: string;
iconData: LucideIconData;
};
export function isLucideIconData(icon: unknown): icon is LucideIconData {
return Array.isArray(icon);
}
export function isLucideIconComponent(icon: unknown): icon is LucideIconComponentType {
return (
icon instanceof Type &&
'iconData' in icon &&
Array.isArray(icon.iconData) &&
'iconName' in icon &&
typeof icon.iconName === 'string'
);
}
export type LucideIconInput = LucideIconComponentType | LucideIconData | string;
export type Nullable<T> = T | null | undefined;
@@ -0,0 +1,3 @@
export function formatFixed(number: number, decimals = 3): string {
return parseFloat(number.toFixed(decimals)).toString(10);
}
@@ -0,0 +1,2 @@
export const toKebabCase = (name: string) =>
name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
+40
View File
@@ -0,0 +1,40 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"paths": {
"@lucide/angular": [
"./dist"
]
},
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.spec.ts"
]
}
+11
View File
@@ -0,0 +1,11 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}
+15
View File
@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"vitest/globals"
]
},
"include": [
"src/**/*.d.ts",
"src/**/*.spec.ts"
]
}
+2 -11
View File
@@ -68,16 +68,7 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Open Collective backers)
### Awesome backers 🍺
<a href="https://github.com/pdfme/pdfme"><img src="https://lucide.dev/sponsors/pdfme.svg" width="180" alt="pdfme – Open-source PDF generation library built with TypeScript and React." /></a>
<a href="https://www.paxhistoria.co/"><img src="https://lucide.dev/sponsors/paxhistoria.svg?" width="180" alt="Pax Historia – An alternate history sandbox game" /></a>
### Backers ☕
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
<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>
+3 -5
View File
@@ -39,20 +39,18 @@
}
},
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons",
"build": "pnpm clean && pnpm copy:license && pnpm build:icons",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts --pretty=false",
"clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts",
"copy:license": "cp ../../LICENSE ./LICENSE",
"copy:utils": "mkdir -p ./src/utils && for f in hasA11yProp toKebabCase mergeClasses; do cp -f ../../packages/shared/src/utils/$f.ts ./src/utils/; done",
"test": "pnpm copy:utils && pnpm build:icons && vitest run",
"test:watch": "pnpm build:icons && vitest run --watch",
"test": "pnpm build:icons && vitest run",
"version": "pnpm version --git-tag-version=false"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
"@lucide/build-icons": "workspace:*",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/jest-dom": "^6.6.3",
"jest-serializer-html": "^7.1.0",
"linkedom": "^0.18.5",
"prettier": "^3.4.2",
-3
View File
@@ -1,8 +1,6 @@
---
import defaultAttributes from './defaultAttributes';
import type { IconProps as Props } from './types';
import { hasA11yProp } from './utils/hasA11yProp';
const {
color = 'currentColor',
@@ -22,7 +20,6 @@ const {
height: size,
stroke: color,
'stroke-width': absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
...(!hasA11yProp(rest) && { 'aria-hidden': 'true' }),
...rest,
}}
class:list={['lucide', className]}
+1 -2
View File
@@ -1,9 +1,8 @@
import { mergeClasses, toKebabCase } from './utils';
import type { AstroComponentFactory } from 'astro/runtime/server/render/astro/factory.js';
import type { IconNode } from './types';
import { render, renderSlot, createComponent, renderComponent } from 'astro/compiler-runtime';
import Icon from './Icon.astro';
import { mergeClasses } from './utils/mergeClasses';
import { toKebabCase } from './utils/toKebabCase';
export default (iconName: string, iconNode: IconNode): AstroComponentFactory => {
const Component = createComponent(
-1
View File
@@ -10,7 +10,6 @@ export interface IconProps extends SVGAttributes {
absoluteStrokeWidth?: boolean;
class?: string;
iconNode?: IconNode;
title?: string;
}
export type SVGAttributes = HTMLAttributes<'svg'>;
@@ -1,10 +1,21 @@
/**
* Converts string to kebab case
*
* @param {string} string
* @returns {string} A kebabized string
*/
export const toKebabCase = (string: string): string =>
string.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
/**
* Merges classes into a single string
*
* @param {array} classes
* @returns {string} A string of classes
*/
export const mergeClasses = <ClassType = string | undefined | null>(...classes: ClassType[]) =>
export const mergeClasses = <ClassType = string | undefined | null>(
...classes: ClassType[]
): string =>
classes
.filter((className, index, array) => {
return (
+2 -43
View File
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { airVent } from './testIconNodes';
import { createAstroHTMLString, render } from './utils';
import { Icon, Rocket } from '../src/lucide-astro';
import { render } from './utils';
import { Icon } from '../src/lucide-astro';
describe('Using Icon Component', async () => {
const { container } = await render(Icon, {
@@ -16,44 +16,3 @@ describe('Using Icon Component', async () => {
expect(container.innerHTML).toBeDefined();
});
});
describe('Icon Component Accessibility', () => {
it('should have aria-hidden prop when no aria prop is present', async () => {
const { container } = await render(Icon, {
props: {
iconNode: airVent,
size: 48,
stroke: 'red',
absoluteStrokeWidth: true,
},
});
expect(container.firstChild).toHaveAttribute('aria-hidden', 'true');
});
it('should not have aria-hidden prop when aria prop is present', async () => {
const { container } = await render(Rocket, {
props: {
size: 48,
stroke: 'red',
absoluteStrokeWidth: true,
'aria-label': 'Release icon',
},
});
expect(container.firstChild).not.toHaveAttribute('aria-hidden');
});
it('should not have aria-hidden prop when title prop is present', async () => {
const { container } = await render(Rocket, {
props: {
size: 48,
stroke: 'red',
absoluteStrokeWidth: true,
title: 'Release icon',
},
});
expect(container.firstChild).not.toHaveAttribute('aria-hidden');
});
});
@@ -10,7 +10,6 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = `
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide"
>
<path d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2">
@@ -10,7 +10,6 @@ exports[`Using createLucideIcon > should create a component from an iconNode 1`]
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-air-vent"
>
<path d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2">
@@ -10,7 +10,6 @@ exports[`Using lucide icon components > should add a non-default attribute to th
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
style="position: absolute"
class="lucide lucide-smile"
>
@@ -46,7 +45,6 @@ exports[`Using lucide icon components > should adjust the size, stroke color and
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
@@ -77,7 +75,6 @@ exports[`Using lucide icon components > should apply all classes to the element
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-droplet my-icon"
>
<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z">
@@ -95,7 +92,6 @@ exports[`Using lucide icon components > should not scale the strokeWidth when ab
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
@@ -126,7 +122,6 @@ exports[`Using lucide icon components > should pass children to the icon slot 1`
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-smile"
>
<circle cx="12"
@@ -164,7 +159,6 @@ exports[`Using lucide icon components > should render a component 1`] = `
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
@@ -195,7 +189,6 @@ exports[`Using lucide icon components > should render the icon with default attr
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-grid-3x3"
>
<rect width="18"
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { Pen, Edit2, Grid, Droplet, Smile, Rocket } from '../src/lucide-astro';
import { Pen, Edit2, Grid, Droplet, Smile } from '../src/lucide-astro';
import defaultAttributes from '../src/defaultAttributes';
import { createAstroHTMLString, render } from './utils';
+2 -11
View File
@@ -67,16 +67,7 @@ Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/licen
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="https://lucide.dev/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Open Collective backers)
### Awesome backers 🍺
<a href="https://github.com/pdfme/pdfme"><img src="https://lucide.dev/sponsors/pdfme.svg" width="180" alt="pdfme – Open-source PDF generation library built with TypeScript and React." /></a>
<a href="https://www.paxhistoria.co/"><img src="https://lucide.dev/sponsors/paxhistoria.svg?" width="180" alt="Pax Historia – An alternate history sandbox game" /></a>
### Backers ☕
<a href="https://www.fina.money/"><img src="https://lucide.dev/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
<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>
+2 -3
View File
@@ -26,13 +26,12 @@
"Font Awesome"
],
"scripts": {
"build": "pnpm clean && pnpm copy:license && pnpm copy:utils &&pnpm build:icons && pnpm build:ng",
"build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:ng",
"copy:license": "cp ../../LICENSE ./LICENSE",
"copy:utils": "mkdir -p ./src/utils && cp -rf ../../packages/shared/src/utils/hasA11yProp.ts ./src/utils/",
"clean": "rm -rf dist && rm -rf ./src/icons/*.ts",
"build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=lucide-icons.ts",
"build:ng": "ng build --configuration production",
"test": "pnpm copy:utils && ng test --no-watch --no-progress --browsers=ChromeHeadlessCI",
"test": "ng test --no-watch --no-progress --browsers=ChromeHeadlessCI",
"test:watch": "ng test",
"lint": "npx eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix",
"e2e": "ng e2e",
@@ -8,10 +8,8 @@ import { LucideIcons } from '../icons/types';
describe('LucideAngularComponent', () => {
let testHostComponent: TestHostComponent;
let testHostFixture: ComponentFixture<TestHostComponent>;
const getAttribute = (attr: string) =>
const getSvgAttribute = (attr: string) =>
testHostFixture.nativeElement.querySelector('svg').getAttribute(attr);
const getRootAttribute = (attr: string) =>
testHostFixture.nativeElement.querySelector('i-lucide').getAttribute(attr);
const testIcons: LucideIcons = {
Demo: [['polyline', { points: '1 1 22 22' }]],
};
@@ -33,28 +31,28 @@ describe('LucideAngularComponent', () => {
it('should add all classes', () => {
testHostFixture.detectChanges();
expect(getAttribute('class')).toBe('lucide lucide-demo my-icon');
expect(getSvgAttribute('class')).toBe('lucide lucide-demo my-icon');
});
it('should set color', () => {
const color = 'red';
testHostComponent.setColor(color);
testHostFixture.detectChanges();
expect(getAttribute('stroke')).toBe(color);
expect(getSvgAttribute('stroke')).toBe(color);
});
it('should set size', () => {
const size = 12;
testHostComponent.setSize(size);
testHostFixture.detectChanges();
expect(getAttribute('width')).toBe(size.toString(10));
expect(getSvgAttribute('width')).toBe(size.toString(10));
});
it('should set stroke width', () => {
const strokeWidth = 1.41;
testHostComponent.setStrokeWidth(strokeWidth);
testHostFixture.detectChanges();
expect(getAttribute('stroke-width')).toBe(strokeWidth.toString(10));
expect(getSvgAttribute('stroke-width')).toBe(strokeWidth.toString(10));
});
it('should adjust stroke width', () => {
@@ -64,61 +62,27 @@ describe('LucideAngularComponent', () => {
testHostComponent.setSize(12);
testHostComponent.setAbsoluteStrokeWidth(true);
testHostFixture.detectChanges();
expect(getAttribute('stroke-width')).toBe(
expect(getSvgAttribute('stroke-width')).toBe(
formatFixed(strokeWidth / (size / defaultAttributes.height)),
);
});
it('should have aria-hidden prop when no aria prop is present', async () => {
testHostFixture.detectChanges();
expect(getRootAttribute('aria-hidden')).toBe('true');
});
it('should not have aria-hidden prop when aria prop is present', async () => {
const ariaLabel = 'Demo icon';
testHostComponent.setAriaLabel(ariaLabel);
testHostFixture.detectChanges();
expect(getRootAttribute('aria-label')).toBe(ariaLabel);
expect(getRootAttribute('aria-hidden')).toBeNull();
});
it('should not have aria-hidden prop when title prop is present', async () => {
const ariaLabel = 'Demo icon';
testHostComponent.setTitle(ariaLabel);
testHostFixture.detectChanges();
expect(getRootAttribute('title')).toBe(ariaLabel);
expect(getRootAttribute('aria-hidden')).toBeNull();
});
it('should never override aria-hidden prop', async () => {
testHostComponent.setAriaHidden(true);
testHostFixture.detectChanges();
expect(getRootAttribute('aria-hidden')).toBe('true');
});
@Component({
selector: 'lucide-spec-host-component',
template: `<i-lucide
template: ` <i-lucide
name="demo"
class="my-icon"
[color]="color"
[size]="size"
[strokeWidth]="strokeWidth"
[absoluteStrokeWidth]="absoluteStrokeWidth"
[attr.aria-label]="ariaLabel"
[attr.title]="title"
[attr.aria-hidden]="ariaHidden"
>
</i-lucide>`,
></i-lucide>`,
})
class TestHostComponent {
color?: string;
size?: number;
strokeWidth?: number;
absoluteStrokeWidth = true;
ariaLabel?: string = undefined;
title?: string = undefined;
ariaHidden?: boolean = undefined;
setColor(color: string): void {
this.color = color;
@@ -135,17 +99,5 @@ describe('LucideAngularComponent', () => {
setAbsoluteStrokeWidth(absoluteStrokeWidth: boolean): void {
this.absoluteStrokeWidth = absoluteStrokeWidth;
}
setAriaLabel(label: string): void {
this.ariaLabel = label;
}
setTitle(title: string): void {
this.title = title;
}
setAriaHidden(ariaHidden: boolean): void {
this.ariaHidden = ariaHidden;
}
}
});
@@ -12,7 +12,6 @@ import { LucideIconData } from '../icons/types';
import defaultAttributes from '../icons/constants/default-attributes';
import { LUCIDE_ICONS, LucideIconProviderInterface } from './lucide-icon.provider';
import { LucideIconConfig } from './lucide-icon.config';
import { hasA11yProp } from '../utils/hasA11yProp';
interface TypedChange<T> extends SimpleChange {
previousValue: T;
@@ -100,12 +99,6 @@ export class LucideAngularComponent implements OnChanges {
this.strokeWidth = this.parseNumber(this.strokeWidth ?? this.iconConfig.strokeWidth);
this.absoluteStrokeWidth = this.absoluteStrokeWidth ?? this.iconConfig.absoluteStrokeWidth;
const nameOrIcon = this.img ?? this.name;
const restAttributes = this.getRestAttributes();
if (!hasA11yProp(restAttributes)) {
this.renderer.setAttribute(this.elem.nativeElement, 'aria-hidden', 'true');
}
if (typeof nameOrIcon === 'string') {
const icoOfName = this.getIcon(this.toPascalCase(nameOrIcon));
if (icoOfName) {
@@ -126,8 +119,6 @@ export class LucideAngularComponent implements OnChanges {
}
replaceElement(img: LucideIconData): void {
const childElements = this.elem.nativeElement.childNodes;
const attributes = {
...defaultAttributes,
width: this.size,
@@ -142,7 +133,6 @@ export class LucideAngularComponent implements OnChanges {
if (typeof this.name === 'string') {
icoElement.classList.add(`lucide-${this.name.replace('_', '-')}`);
}
if (this.class) {
icoElement.classList.add(
...this.class
@@ -151,21 +141,13 @@ export class LucideAngularComponent implements OnChanges {
.filter((a) => a.length > 0),
);
}
const childElements = this.elem.nativeElement.childNodes;
for (const child of childElements) {
this.renderer.removeChild(this.elem.nativeElement, child);
}
this.renderer.appendChild(this.elem.nativeElement, icoElement);
}
getRestAttributes(): Record<string, string> {
const restAttributeMap: NamedNodeMap = this.elem.nativeElement.attributes;
const restAttributes = Object.fromEntries(
Array.from(restAttributeMap).map((item) => [item.name, item.value]),
);
return restAttributes;
}
toPascalCase(str: string): string {
return str.replace(
/(\w)([a-z0-9]*)(_|-|\s*)/g,
@@ -0,0 +1,101 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { formatFixed, LucideIcon } from './lucide-icon.component';
import defaultAttributes from '../icons/constants/default-attributes';
import { LucideIconData } from '../icons/types';
describe('LucideAngularComponent', () => {
let testHostComponent: TestHostComponent;
let testHostFixture: ComponentFixture<TestHostComponent>;
const getSvgAttribute = (attr: string) =>
testHostFixture.nativeElement.querySelector('svg').getAttribute(attr);
const testIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LucideIcon, TestHostComponent],
imports: [],
}).compileComponents();
testHostFixture = TestBed.createComponent(TestHostComponent);
testHostComponent = testHostFixture.componentInstance;
});
it('should create', () => {
testHostFixture.detectChanges();
expect(testHostComponent).toBeTruthy();
});
it('should add all classes', () => {
testHostFixture.detectChanges();
expect(getSvgAttribute('class')).toBe('lucide lucide-demo my-icon');
});
it('should set color', () => {
const color = 'red';
testHostComponent.setColor(color);
testHostFixture.detectChanges();
expect(getSvgAttribute('stroke')).toBe(color);
});
it('should set size', () => {
const size = 12;
testHostComponent.setSize(size);
testHostFixture.detectChanges();
expect(getSvgAttribute('width')).toBe(size.toString(10));
});
it('should set stroke width', () => {
const strokeWidth = 1.41;
testHostComponent.setStrokeWidth(strokeWidth);
testHostFixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe(strokeWidth.toString(10));
});
it('should adjust stroke width', () => {
const strokeWidth = 2;
const size = 12;
testHostComponent.setStrokeWidth(strokeWidth);
testHostComponent.setSize(12);
testHostComponent.setAbsoluteStrokeWidth(true);
testHostFixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe(
formatFixed(strokeWidth / (size / defaultAttributes.height))
);
});
@Component({
selector: 'lucide-spec-host-component',
template: ` <i-lucide
name="demo"
[img]="testIcon"
class="my-icon"
[color]="color"
[size]="size"
[strokeWidth]="strokeWidth"
[absoluteStrokeWidth]="absoluteStrokeWidth"
></i-lucide>`,
})
class TestHostComponent {
color?: string;
size?: number;
strokeWidth?: number;
absoluteStrokeWidth = true;
readonly testIcon = testIcon;
setColor(color: string): void {
this.color = color;
}
setSize(size: number): void {
this.size = size;
}
setStrokeWidth(strokeWidth: number): void {
this.strokeWidth = strokeWidth;
}
setAbsoluteStrokeWidth(absoluteStrokeWidth: boolean): void {
this.absoluteStrokeWidth = absoluteStrokeWidth;
}
}
});
@@ -0,0 +1,213 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
Inject,
Input,
OnChanges,
OnInit,
Renderer2,
SimpleChange,
Type,
} from '@angular/core';
import { LucideIconData } from '../icons/types';
import defaultAttributes from '../icons/constants/default-attributes';
import { LucideIconConfig } from './lucide-icon.config';
interface TypedChange<T> extends SimpleChange {
previousValue: T;
currentValue: T;
}
type SvgAttributes = { [key: string]: string | number };
type LucideAngularComponentChanges = {
name?: TypedChange<string | LucideIconData>;
icon?: TypedChange<LucideIconData | undefined>;
color?: TypedChange<string>;
size?: TypedChange<number>;
strokeWidth?: TypedChange<number>;
absoluteStrokeWidth?: TypedChange<boolean>;
class: TypedChange<string>;
};
export function formatFixed(number: number, decimals = 3): string {
return parseFloat(number.toFixed(decimals)).toString(10);
}
export type LucideIconComponentType = Type<LucideIcon> & { iconData: LucideIconData; name: string };
function isLucideIconComponent(icon: unknown): icon is LucideIconComponentType {
return (
icon instanceof Type &&
'iconData' in icon &&
Array.isArray(icon.iconData) &&
'iconName' in icon &&
typeof icon.iconName === 'string'
);
}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'svg[lucideIcon]',
template: '<ng-content></ng-content>',
standalone: true,
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class LucideIcon implements OnInit, OnChanges {
@Input() class?: string;
_name?: string;
@Input() set name(name: string | undefined) {
this._name = name;
}
get name() {
return this._name;
}
_icon?: LucideIconData | LucideIconComponentType | null;
@Input('lucideIcon') set icon(icon: LucideIconData | LucideIconComponentType | null | undefined) {
this._icon = icon;
}
get icon() {
return this._icon;
}
@Input() color?: string;
@Input() absoluteStrokeWidth = false;
defaultSize: number;
constructor(
@Inject(ElementRef) protected elem: ElementRef,
@Inject(Renderer2) protected renderer: Renderer2,
@Inject(ChangeDetectorRef) protected changeDetector: ChangeDetectorRef,
@Inject(LucideIconConfig) protected iconConfig: LucideIconConfig
) {
this.defaultSize = defaultAttributes.height;
}
_size?: number;
get size(): number {
return this._size ?? this.iconConfig.size;
}
@Input() set size(value: string | number | undefined) {
if (value) {
this._size = this.parseNumber(value);
} else {
delete this._size;
}
}
_strokeWidth?: number;
get strokeWidth(): number {
return this._strokeWidth ?? this.iconConfig.strokeWidth;
}
@Input() set strokeWidth(value: string | number | undefined) {
if (value) {
this._strokeWidth = this.parseNumber(value);
} else {
delete this._strokeWidth;
}
}
ngOnInit() {
this.buildIcon();
}
ngOnChanges(changes: LucideAngularComponentChanges): void {
if (
changes.name ||
changes.icon ||
changes.color ||
changes.size ||
changes.absoluteStrokeWidth ||
changes.strokeWidth ||
changes.class
) {
this.buildIcon();
}
this.changeDetector.markForCheck();
}
buildIcon(): void {
this.color = this.color ?? this.iconConfig.color;
this.size = this.parseNumber(this.size ?? this.iconConfig.size);
this.strokeWidth = this.parseNumber(this.strokeWidth ?? this.iconConfig.strokeWidth);
this.absoluteStrokeWidth = this.absoluteStrokeWidth ?? this.iconConfig.absoluteStrokeWidth;
console.log('Hello, my name is ', this.name, ' my icon is ', this.icon);
if (this.icon) {
this.replaceElement(isLucideIconComponent(this.icon) ? this.icon.iconData : this.icon);
}
}
replaceElement(img: LucideIconData): void {
const attributes = {
...defaultAttributes,
width: this.size,
height: this.size,
stroke: this.color ?? this.iconConfig.color,
'stroke-width': this.absoluteStrokeWidth
? formatFixed(this.strokeWidth / (this.size / this.defaultSize))
: this.strokeWidth.toString(10),
};
const icoElement = this.elem.nativeElement;
for (const [name, value] of Object.entries(attributes)) {
icoElement.setAttribute(name, value);
}
icoElement.classList.add('lucide');
if (typeof this.name === 'string') {
icoElement.classList.add(`lucide-${this.name.replace('_', '-')}`);
}
if (this.class) {
icoElement.classList.add(
...this.class
.split(/ /)
.map((a) => a.trim())
.filter((a) => a.length > 0)
);
}
for (const child of icoElement.children) {
this.renderer.removeChild(this.elem.nativeElement, child);
}
for (const node of img) {
const childElement = this.createElement(node);
this.renderer.appendChild(icoElement, childElement);
}
}
protected parseNumber(value: string | number): number {
if (typeof value === 'string') {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new Error(`${value} is not numeric.`);
}
return parsedValue;
}
return value;
}
protected createElement([tag, attrs, children = []]: readonly [
string,
SvgAttributes,
LucideIconData?
]) {
const element = this.renderer.createElement(tag, 'http://www.w3.org/2000/svg');
Object.keys(attrs).forEach((name) => {
const attrValue: string =
typeof attrs[name] === 'string' ? (attrs[name] as string) : attrs[name].toString(10);
this.renderer.setAttribute(element, name, attrValue);
});
if (children.length) {
children.forEach((child) => {
const childElement = this.createElement(child);
this.renderer.appendChild(element, childElement);
});
}
return element;
}
}

Some files were not shown because too many files have changed in this diff Show More