Compare commits

...

8 Commits

Author SHA1 Message Date
Karsa
a784e9922c docs(packages/angular): added (for now) full documentation for @lucide/angular 2025-12-19 12:04:37 +01:00
Karsa
1a1843cb2f feat(packages/angular): update angular.json 2025-12-19 09:56:19 +01:00
Karsa
7ced22b514 feat(packages/angular): update readme 2025-12-19 09:54:55 +01:00
Karsa
1b72561da4 feat(packages/angular): added initial @lucide/angular package 2025-12-19 09:40:38 +01:00
Eric Fennis
1075461aab Add new lucide angular package 2025-12-13 20:12:09 +01:00
Jakob Guddas
0b8f99326c fix(icons): changed paint-bucket icon (#3880)
* Updated icons/paint-bucket.svg

* Updated icons/paint-bucket.svg

* Updated icons/paint-bucket.svg

* Updated icons/paint-bucket.svg

* Updated icons/paint-bucket.json

* Updated icons/paint-bucket.json
2025-12-12 13:27:37 +01:00
Alexandru Portan
7abb61630e feat(icons): added stone icon (#3850)
* Added icons/stone.svg

* Added icons/stone.json

* Update icons/stone.json

Added suggested tags

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update stone.svg

Updated based on suggested changes

* Update icons/stone.json

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jakob Guddas <github@jguddas.de>
2025-12-12 09:26:54 +01:00
Eric Fennis
3b0d158ea1 fix(site): Small adjustments color picker and add clear button search bar (#3851)
* Small adjustments

* Format code

* format code

* Remove default value

* format code

* update yml file

* Format code

* Update docs/.vitepress/theme/components/base/Input.vue

Co-authored-by: Karsa <contact@karsa.org>

* Add extra check if null or undefined

---------

Co-authored-by: Karsa <contact@karsa.org>
2025-12-12 09:25:39 +01:00
58 changed files with 6196 additions and 510 deletions

View File

@@ -13,16 +13,19 @@ 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 - label: lucide-angular (old version)
- 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-svelte - label: lucide-static
- 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: Figma plugin - label: Figma plugin
- label: source/main - label: source/main
- label: other/not relevant - label: other/not relevant

View File

@@ -13,19 +13,23 @@ 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 - label: lucide-angular (old version)
- 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-svelte - label: lucide-static
- 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: 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

1
.github/labeler.yml vendored
View File

@@ -59,6 +59,7 @@
🅰️ 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

41
.github/workflows/angular.yml vendored Normal file
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

View File

@@ -58,6 +58,7 @@ jobs:
'lucide-preact', 'lucide-preact',
'lucide-solid', 'lucide-solid',
'lucide-svelte', 'lucide-svelte',
'@lucide/angular',
'@lucide/astro', '@lucide/astro',
'@lucide/svelte', '@lucide/svelte',
] ]

View File

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

View File

@@ -1,5 +1,5 @@
import { createLucideIcon } from 'lucide-react/src/lucide-react'; import { createLucideIcon } from 'lucide-react/src/lucide-react';
import { type LucideProps, type IconNode } from 'lucide-react/src/createLucideIcon'; import { type LucideProps, type IconNode } from 'lucide-react/src/types';
import { IconEntity } from '../theme/types'; import { IconEntity } from '../theme/types';
import { renderToStaticMarkup } from 'react-dom/server'; import { renderToStaticMarkup } from 'react-dom/server';
import { IconContent } from './generateZip'; import { IconContent } from './generateZip';

View File

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

View File

@@ -1,23 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { useData } from 'vitepress';
import { computed } from 'vue'; import { computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
modelValue: string modelValue: string;
id: string id: string;
}>() }>();
const emit = defineEmits(['update:modelValue']) const { isDark } = useData();
const emit = defineEmits(['update:modelValue']);
const value = computed({ const value = computed({
get: () => props.modelValue, get: () => {
set: (val) => emit('update:modelValue', val) if (props.modelValue == null || props.modelValue === 'currentColor') {
}) return isDark.value ? '#ffffff' : '#000000';
}
return props.modelValue;
},
set: (val) => emit('update:modelValue', val),
});
</script> </script>
<template> <template>
<div class="color-picker"> <div class="color-picker">
<div class="color-input-wrapper"> <div class="color-input-wrapper">
<!-- TODO: Add currentColor div if value is currentColor -->
<input <input
type="color" type="color"
:id="id" :id="id"
@@ -33,6 +41,7 @@ const value = computed({
class="color-input-text" class="color-input-text"
aria-label="Color picker input" aria-label="Color picker input"
v-model="value" v-model="value"
placeholder="[default]"
/> />
</div> </div>
</template> </template>
@@ -45,19 +54,21 @@ const value = computed({
top: -5px; top: -5px;
left: -5px; left: -5px;
} }
.color-input-wrapper { .color-input-wrapper {
height: 24px; height: 24px;
width: 24px; width: 24px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
border-radius: 12px; border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
} }
.color-picker { .color-picker {
background: var(--color-picker-bg, var(--vp-c-bg-alt)); background: var(--color-picker-bg, var(--vp-c-bg-alt));
border-radius: 8px; border-radius: 8px;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);
padding: 4px 8px; padding: 3px 8px 3px 3px;
height: auto; height: auto;
font-size: 14px; font-size: 14px;
text-align: left; text-align: left;
@@ -66,6 +77,10 @@ const value = computed({
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
} }
.color-input-text { .color-input-text {
@@ -79,15 +94,18 @@ const value = computed({
text-align: left; text-align: left;
border-radius: 8px; border-radius: 8px;
cursor: text; cursor: text;
transition: border-color 0.25s, background 0.4s ease; transition:
border-color 0.25s,
background 0.4s ease;
letter-spacing: 1px;
} }
.color-picker:hover, .color-picker:focus { .color-picker:hover,
.color-picker:focus {
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
} }
.color-input[value="currentColor"] { .color-input[value='currentColor'] {
} }
</style> </style>

View File

@@ -1,22 +1,27 @@
<script setup> <script setup>
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon' import Icon from 'lucide-vue-next/src/Icon';
import { search } from '../../../data/iconNodes' import { search } from '../../../data/iconNodes';
const SearchIcon = createLucideIcon('search', search)
defineProps({ defineProps({
shortcut: { shortcut: {
type: String, type: String,
required: false required: false,
} },
}) });
</script> </script>
<template> <template>
<button class="fake-input"> <button class="fake-input">
<component :is="SearchIcon" class="search-icon"/> <Icon
<slot/> :iconNode="search"
<kbd v-if="shortcut" class="shortcut">{{ shortcut }}</kbd> class="search-icon"
/>
<slot />
<kbd
v-if="shortcut"
class="shortcut"
>{{ shortcut }}</kbd
>
</button> </button>
</template> </template>
@@ -34,10 +39,14 @@ defineProps({
cursor: text; cursor: text;
display: flex; display: flex;
gap: 12px; gap: 12px;
transition: color 0.25s, border-color 0.25s, background-color 0.25s; transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
} }
.fake-input:hover, .fake-input:focus { .fake-input:hover,
.fake-input:focus {
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
} }

View File

@@ -5,7 +5,6 @@
</template> </template>
<style scoped> <style scoped>
.icon-button { .icon-button {
display: inline-flex; display: inline-flex;
border: 1px solid transparent; border: 1px solid transparent;
@@ -30,9 +29,9 @@
} }
.icon-button:active { .icon-button:active {
border-color: var(--vp-button-alt-active-border); border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text); color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg); background-color: var(--vp-button-alt-active-bg);
} }
.icon-button.active { .icon-button.active {

View File

@@ -1,60 +1,90 @@
<script lang="ts"> <script lang="ts">
export default { export default {
inheritAttrs: false, inheritAttrs: false,
} };
export interface InputProps { export interface InputProps {
type: string type: string;
modelValue: string modelValue: string;
shortcut?: string shortcut?: string;
} }
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick, watch } from 'vue' import { ref, onMounted, nextTick, watch } from 'vue';
import Icon from 'lucide-vue-next/src/Icon';
import { x } from '../../../data/iconNodes';
import IconButton from './IconButton.vue';
const props = withDefaults(defineProps<InputProps>(), { const props = withDefaults(defineProps<InputProps>(), {
type: 'text' type: 'text',
}) });
const input = ref() const input = ref();
const wrapperEl = ref() const wrapperEl = ref();
const shortcutEl = ref() const shortcutEl = ref();
defineEmits(['change', 'input', 'update:modelValue']) const emit = defineEmits(['change', 'input', 'update:modelValue']);
const updateShortcutSpacing = () => { const updateShortcutSpacing = () => {
nextTick(() => { nextTick(() => {
if (shortcutEl.value && wrapperEl.value) { if (shortcutEl.value && wrapperEl.value) {
const shortcutWidth = shortcutEl.value.offsetWidth const shortcutWidth = shortcutEl.value.offsetWidth;
wrapperEl.value.style.setProperty('--shortcut-width', `${shortcutWidth}px`) wrapperEl.value.style.setProperty('--shortcut-width', `${shortcutWidth}px`);
} }
}) });
} };
onMounted(updateShortcutSpacing) onMounted(updateShortcutSpacing);
watch(() => props.shortcut, updateShortcutSpacing) watch(() => props.shortcut, updateShortcutSpacing);
function onClear() {
emit('update:modelValue', '');
input.value.focus();
}
defineExpose({ defineExpose({
focus: () => { focus: () => {
input.value.focus() input.value.focus();
} },
}) });
</script> </script>
<template> <template>
<div class="input-wrapper" ref="wrapperEl"> <div
<slot name="icon" class="icon" /> class="input-wrapper"
ref="wrapperEl"
>
<slot
name="icon"
class="icon"
/>
<input <input
:type="type" :type="type"
class="input" class="input"
:class="{'has-icon': $slots.icon, 'has-shortcut': shortcut}" :class="{ 'has-icon': $slots.icon, 'has-shortcut': shortcut }"
ref="input" ref="input"
:value="modelValue" :value="modelValue"
v-bind="$attrs" v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
/> />
<kbd v-if="shortcut" class="shortcut" ref="shortcutEl">{{ shortcut }}</kbd> <IconButton
@click="onClear"
v-if="type === 'search' && modelValue"
class="clear-button"
aria-label="Clear input"
>
<Icon
:iconNode="x"
:size="20"
/>
</IconButton>
<kbd
v-if="shortcut"
class="shortcut"
ref="shortcutEl"
>{{ shortcut }}</kbd
>
</div> </div>
</template> </template>
@@ -62,6 +92,7 @@ defineExpose({
.input-wrapper { .input-wrapper {
position: relative; position: relative;
} }
.input { .input {
justify-content: flex-start; justify-content: flex-start;
border: 1px solid transparent; border: 1px solid transparent;
@@ -71,13 +102,18 @@ defineExpose({
height: 40px; height: 40px;
background-color: var(--vp-c-bg-alt); background-color: var(--vp-c-bg-alt);
font-size: 14px; font-size: 14px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
} }
.input.has-shortcut { .input.has-shortcut {
padding-right: calc(var(--shortcut-width, 40px) + 22px); padding-right: calc(var(--shortcut-width, 40px) + 22px);
} }
.input:hover, .input:focus { .input:hover,
.input:focus {
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
} }
@@ -86,6 +122,14 @@ defineExpose({
padding-left: 52px; padding-left: 52px;
} }
.clear-button {
position: absolute;
right: 56px;
top: 9px;
padding: 4px;
transition: background-color .25s;
}
.shortcut { .shortcut {
position: absolute; position: absolute;
right: 12px; right: 12px;
@@ -111,7 +155,7 @@ defineExpose({
</style> </style>
<style> <style>
.input-wrapper svg { .input-wrapper > svg {
position: absolute; position: absolute;
left: 16px; left: 16px;
top: 12px; top: 12px;

View File

@@ -1,38 +1,36 @@
<script lang="ts"> <script lang="ts">
export default { export default {
inheritAttrs: false, inheritAttrs: false,
} };
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue';
import Input from './Input.vue' import Input from './Input.vue';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon' import Icon from 'lucide-vue-next/src/Icon';
import { search } from '../../../data/iconNodes' import { search } from '../../../data/iconNodes';
const SearchIcon = createLucideIcon('search', search)
interface Props { interface Props {
modelValue: string modelValue: string;
shortcut?: string shortcut?: string;
} }
const props = defineProps<Props>() const props = defineProps<Props>();
const input = ref() const input = ref();
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue']);
defineExpose({ defineExpose({
focus: () => { focus: () => {
input.value.focus() input.value.focus();
} },
}) });
const value = computed({ const value = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => emit('update:modelValue', val) set: (val) => emit('update:modelValue', val),
}) });
</script> </script>
<template> <template>
@@ -46,7 +44,10 @@ const value = computed({
class="input-wrapper" class="input-wrapper"
> >
<template #icon> <template #icon>
<component :is="SearchIcon" class="search-icon" /> <Icon
:iconNode="search"
class="search-icon"
/>
</template> </template>
</Input> </Input>
</template> </template>
@@ -62,7 +63,8 @@ const value = computed({
background-color: var(--vp-c-bg-alt); background-color: var(--vp-c-bg-alt);
} }
.input:hover, .input:focus { .input:hover,
.input:focus {
border-color: var(--vp-c-brand); border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt); background: var(--vp-c-bg-alt);
} }

View File

@@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { rotateCw } from '../../../data/iconNodes' import { rotateCw } from '../../../data/iconNodes';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon' import Icon from 'lucide-vue-next/src/Icon';
import IconButton from "./IconButton.vue"; import IconButton from './IconButton.vue';
const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
</script> </script>
<template> <template>
<IconButton class="reset-button"> <IconButton class="reset-button">
<RotateIcon :size="20"/> <Icon
:size="20"
:iconNode="rotateCw"
/>
</IconButton> </IconButton>
</template> </template>
@@ -32,6 +33,7 @@ const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
transform: rotate(359deg); transform: rotate(359deg);
} }

View File

@@ -22,18 +22,18 @@ export default {
logo: '/framework-logos/svelte.svg', logo: '/framework-logos/svelte.svg',
label: 'Lucide documentation for Svelte', label: 'Lucide documentation for Svelte',
}, },
{
name: 'lucide-preact',
logo: '/framework-logos/preact.svg',
label: 'Lucide documentation for Preact',
},
{ {
name: 'lucide-solid', name: 'lucide-solid',
logo: '/framework-logos/solid.svg', logo: '/framework-logos/solid.svg',
label: 'Lucide documentation for Solid', label: 'Lucide documentation for Solid',
}, },
{ {
name: 'lucide-angular', name: 'lucide-preact',
logo: '/framework-logos/preact.svg',
label: 'Lucide documentation for Preact',
},
{
name: 'angular',
logo: '/framework-logos/angular.svg', logo: '/framework-logos/angular.svg',
label: 'Lucide documentation for Angular', label: 'Lucide documentation for Angular',
}, },
@@ -48,11 +48,6 @@ export default {
logo: '/framework-logos/react-native.svg', logo: '/framework-logos/react-native.svg',
label: 'Lucide documentation for React Native', label: 'Lucide documentation for React Native',
}, },
{
name: 'lucide-flutter',
logo: '/framework-logos/flutter.svg',
label: 'Lucide documentation for Flutter',
},
], ],
}; };
}, },

View File

@@ -2,45 +2,48 @@
import { useData } from 'vitepress'; import { useData } from 'vitepress';
import { useSessionStorage } from '@vueuse/core'; import { useSessionStorage } from '@vueuse/core';
import IconButton from '../base/IconButton.vue'; import IconButton from '../base/IconButton.vue';
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue' import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue';
import { x } from '../../../data/iconNodes' import { x } from '../../../data/iconNodes';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'; import Icon from 'lucide-vue-next/src/Icon';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
const { theme } = useData() const { theme } = useData();
const showAd = useSessionStorage('show-carbon-ads', true) const showAd = useSessionStorage('show-carbon-ads', true);
const carbonLoaded = ref(true) const carbonLoaded = ref(true);
defineProps<{ defineProps<{
drawerOpen: boolean drawerOpen: boolean;
}>() }>();
const CloseIcon = createLucideIcon('Close', x)
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
if (window?._carbonads == null) { if (window?._carbonads == null) {
carbonLoaded.value = false carbonLoaded.value = false;
} }
}, 5000) }, 5000);
}) });
</script> </script>
<template> <template>
<div <div
:class="{ :class="{
'drawer-open': drawerOpen, 'drawer-open': drawerOpen,
'hide-ad': !(showAd && carbonLoaded) 'hide-ad': !(showAd && carbonLoaded),
}" }"
class="floating-ad" class="floating-ad"
v-if="theme.carbonAds" v-if="theme.carbonAds"
> >
<IconButton @click="showAd = false" class="hide-button"> <IconButton
<component :is="CloseIcon" :size="20" absoluteStrokeWidth /> @click="showAd = false"
class="hide-button"
>
<Icon
:iconNode="x"
:size="20"
absoluteStrokeWidth
/>
</IconButton> </IconButton>
<VPDocAsideCarbonAds <VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" />
:carbon-ads="theme.carbonAds"
/>
</div> </div>
</template> </template>
@@ -51,7 +54,9 @@ onMounted(() => {
bottom: 32px; bottom: 32px;
width: 224px; width: 224px;
right: 32px; right: 32px;
transition: opacity 0.5s, transform 0.25s ease; transition:
opacity 0.5s,
transform 0.25s ease;
} }
.floating-ad.drawer-open { .floating-ad.drawer-open {
@@ -67,8 +72,11 @@ onMounted(() => {
transform: translateY(-288px) translateX(224px); transform: translateY(-288px) translateX(224px);
} }
.floating-ad.drawer-open, .floating-ad.hide-ad { .floating-ad.drawer-open,
transition: opacity 0.25s, transform 0.5s cubic-bezier(0.19, 1, 0.22, 1); .floating-ad.hide-ad {
transition:
opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
} }
@media (min-width: 1280px) { @media (min-width: 1280px) {

View File

@@ -1,70 +1,68 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import ButtonMenu from '../base/ButtonMenu.vue' import ButtonMenu from '../base/ButtonMenu.vue';
import { useIconStyleContext } from '../../composables/useIconStyle'; import { useIconStyleContext } from '../../composables/useIconStyle';
import useConfetti from '../../composables/useConfetti'; import useConfetti from '../../composables/useConfetti';
import getSVGIcon from '../../utils/getSVGIcon'; import getSVGIcon from '../../utils/getSVGIcon';
import downloadData from '../../utils/downloadData'; import downloadData from '../../utils/downloadData';
const downloadText = 'Download!' const downloadText = 'Download!';
const copiedText = 'Copied!' const copiedText = 'Copied!';
const confettiText = ref(copiedText) const confettiText = ref(copiedText);
const props = defineProps<{ const props = defineProps<{
name: string name: string;
popoverPosition?: 'top' | 'bottom' popoverPosition?: 'top' | 'bottom';
}>() }>();
const { size } = useIconStyleContext() const { size } = useIconStyleContext();
const { animate, confetti } = useConfetti() const { animate, confetti } = useConfetti();
function copySVG() { function copySVG() {
confettiText.value = copiedText confettiText.value = copiedText;
const svgString = getSVGIcon() const svgString = getSVGIcon();
navigator.clipboard.writeText(svgString) navigator.clipboard.writeText(svgString);
confetti() confetti();
} }
function copyDataUrl() { function copyDataUrl() {
confettiText.value = copiedText confettiText.value = copiedText;
const svgString = getSVGIcon() const svgString = getSVGIcon();
// Create SVG data url // Create SVG data url
const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}` const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
navigator.clipboard.writeText(dataUrl) navigator.clipboard.writeText(dataUrl);
confetti() confetti();
} }
function downloadSVG() { function downloadSVG() {
confettiText.value = downloadText confettiText.value = downloadText;
const svgString = getSVGIcon() const svgString = getSVGIcon();
downloadData(`${props.name}.svg`, `data:image/svg+xml;base64,${btoa(svgString)}`) downloadData(`${props.name}.svg`, `data:image/svg+xml;base64,${btoa(svgString)}`);
confetti() confetti();
} }
function downloadPNG() { function downloadPNG() {
confettiText.value = downloadText confettiText.value = downloadText;
const svgString = getSVGIcon() const svgString = getSVGIcon();
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = size.value; canvas.width = size.value;
canvas.height = size.value; canvas.height = size.value;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext('2d');
const image = new Image(); const image = new Image();
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`; image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
image.onload = function() { image.onload = function () {
ctx.drawImage(image, 0, 0); ctx.drawImage(image, 0, 0);
downloadData(`${props.name}.png`, canvas.toDataURL('image/png')) downloadData(`${props.name}.png`, canvas.toDataURL('image/png'));
confetti() confetti();
} };
} }
</script> </script>
<template> <template>
@@ -75,10 +73,10 @@ function downloadPNG() {
:data-confetti-text="confettiText" :data-confetti-text="confettiText"
:popoverPosition="popoverPosition" :popoverPosition="popoverPosition"
:options="[ :options="[
{ text: 'Copy SVG' , onClick: copySVG }, { text: 'Copy SVG', onClick: copySVG },
{ text: 'Copy Data URL' , onClick: copyDataUrl }, { text: 'Copy Data URL', onClick: copyDataUrl },
{ text: 'Download SVG' , onClick: downloadSVG }, { text: 'Download SVG', onClick: downloadSVG },
{ text: 'Download PNG' , onClick: downloadPNG }, { text: 'Download PNG', onClick: downloadPNG },
]" ]"
/> />
</template> </template>

View File

@@ -1,41 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, useSlots } from 'vue'; import { computed, useSlots } from 'vue';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon' import { copy } from '../../../data/iconNodes';
import { copy } from '../../../data/iconNodes'
import useConfetti from '../../composables/useConfetti'; import useConfetti from '../../composables/useConfetti';
const { animate, confetti } = useConfetti() import Icon from 'lucide-vue-next/src/Icon';
const slots = useSlots() const { animate, confetti } = useConfetti();
const slots = useSlots();
const copiedText = computed(() => slots.default?.()[0].children) const copiedText = computed(() => slots.default?.()[0].children);
function copyText() { function copyText() {
navigator.clipboard.writeText(copiedText.value) navigator.clipboard.writeText(copiedText.value);
confetti() confetti();
} }
const Copy = createLucideIcon('ChevronUp', copy)
</script> </script>
<template> <template>
<h1 <h1
class="icon-name confetti-button" class="icon-name confetti-button"
:class="{animate}" :class="{ animate }"
data-confetti-text="Copied!" data-confetti-text="Copied!"
@click="copyText" @click="copyText"
> >
<slot /> <slot />
<Copy :size="20" class="copy-icon"/> <Icon
:iconNode="copy"
:size="20"
class="copy-icon"
/>
</h1> </h1>
</template> </template>
<style scoped> <style scoped>
@import './confetti.css'; @import './confetti.css';
.icon-name { .icon-name {
font-size: 24px; font-size: 24px;
font-weight: 500; font-weight: 500;
line-height: 32px; line-height: 32px;
transition: background ease-in .15s;; transition: background ease-in 0.15s;
padding: 2px 8px; padding: 2px 8px;
border-radius: 8px; border-radius: 8px;
width: auto; width: auto;
@@ -48,7 +51,7 @@ const Copy = createLucideIcon('ChevronUp', copy)
} }
.icon-name:hover .copy-icon { .icon-name:hover .copy-icon {
opacity: .9; opacity: 0.9;
} }
.icon-name:before, .icon-name:before,
@@ -65,10 +68,10 @@ const Copy = createLucideIcon('ChevronUp', copy)
opacity: 0; opacity: 0;
margin-left: 12px; margin-left: 12px;
margin-top: 6px; margin-top: 6px;
transition:ease .3s opacity; transition: ease 0.3s opacity;
} }
.icon-name:hover .copy-icon:hover { .icon-name:hover .copy-icon:hover {
opacity: .6; opacity: 0.6;
} }
</style> </style>

View File

@@ -1,75 +1,72 @@
<script setup lang="ts"> <script setup lang="ts">
import { shallowRef, type Ref, watch, computed } from 'vue' import { shallowRef, type Ref, watch, computed } from 'vue';
import { useCssVar, syncRef } from '@vueuse/core' import { useCssVar, syncRef } from '@vueuse/core';
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle' import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle';
import RangeSlider from '../base/RangeSlider.vue' import RangeSlider from '../base/RangeSlider.vue';
import InputField from '../base/InputField.vue' import InputField from '../base/InputField.vue';
import ColorPicker from '../base/ColorPicker.vue' import ColorPicker from '../base/ColorPicker.vue';
import ResetButton from '../base/ResetButton.vue' import ResetButton from '../base/ResetButton.vue';
import Switch from '../base/Switch.vue' import Switch from '../base/Switch.vue';
const props = defineProps<{ const props = defineProps<{
rootEl?: Ref<HTMLElement> rootEl?: Ref<HTMLElement>;
}>() }>();
const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext() const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext();
const documentRef = shallowRef<HTMLElement | undefined>(typeof document !== 'undefined' ? document?.documentElement : undefined) const documentRef = shallowRef<HTMLElement | undefined>(
typeof document !== 'undefined' ? document?.documentElement : undefined,
);
const colorCssVar = useCssVar( const colorCssVar = useCssVar('--customize-color', props.rootEl?.value ?? documentRef.value, {
'--customize-color', initialValue: `${STYLE_DEFAULTS.color}`,
props.rootEl?.value ?? documentRef.value, });
{
initialValue: `${STYLE_DEFAULTS.color}`
}
)
const strokeWidthCssVar = useCssVar( const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth', '--customize-strokeWidth',
props.rootEl?.value ?? documentRef.value, props.rootEl?.value ?? documentRef.value,
{ {
initialValue: `${STYLE_DEFAULTS.strokeWidth}` initialValue: `${STYLE_DEFAULTS.strokeWidth}`,
} },
) );
const sizeCssVar = useCssVar( const sizeCssVar = useCssVar('--customize-size', props.rootEl?.value ?? documentRef.value, {
'--customize-size', initialValue: `${STYLE_DEFAULTS.size}`,
props.rootEl?.value ?? documentRef.value, });
{
initialValue: `${STYLE_DEFAULTS.size}`
}
)
syncRef(color, colorCssVar, { direction: 'ltr' }) syncRef(color, colorCssVar, { direction: 'ltr' });
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' }) syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' });
syncRef(size, sizeCssVar, { direction: 'ltr' }) syncRef(size, sizeCssVar, { direction: 'ltr' });
function resetStyle () { function resetStyle() {
color.value = STYLE_DEFAULTS.color color.value = STYLE_DEFAULTS.color;
strokeWidth.value = STYLE_DEFAULTS.strokeWidth strokeWidth.value = STYLE_DEFAULTS.strokeWidth;
size.value = STYLE_DEFAULTS.size size.value = STYLE_DEFAULTS.size;
absoluteStrokeWidth.value = STYLE_DEFAULTS.absoluteStrokeWidth absoluteStrokeWidth.value = STYLE_DEFAULTS.absoluteStrokeWidth;
} }
watch(absoluteStrokeWidth, (enabled) => { watch(absoluteStrokeWidth, (enabled) => {
const htmlEl = document.documentElement const htmlEl = document.documentElement;
htmlEl.classList.toggle('absolute-stroke-width', enabled) htmlEl.classList.toggle('absolute-stroke-width', enabled);
}) });
const customizingActive = computed(() => { const customizingActive = computed(() => {
return color.value !== STYLE_DEFAULTS.color return (
|| strokeWidth.value !== STYLE_DEFAULTS.strokeWidth color.value !== STYLE_DEFAULTS.color ||
|| size.value !== STYLE_DEFAULTS.size strokeWidth.value !== STYLE_DEFAULTS.strokeWidth ||
|| absoluteStrokeWidth.value !== STYLE_DEFAULTS.absoluteStrokeWidth size.value !== STYLE_DEFAULTS.size ||
}) absoluteStrokeWidth.value !== STYLE_DEFAULTS.absoluteStrokeWidth
);
});
</script> </script>
<template> <template>
<div class="customizer-card" :class="{ customized: customizingActive }"> <div
class="customizer-card"
:class="{ customized: customizingActive }"
>
<div class="card-header"> <div class="card-header">
<h2 class="card-title"> <h2 class="card-title">Customizer</h2>
Customizer
</h2>
<ResetButton @click="resetStyle"></ResetButton> <ResetButton @click="resetStyle"></ResetButton>
</div> </div>
<InputField <InputField
@@ -77,7 +74,11 @@ const customizingActive = computed(() => {
label="Color" label="Color"
> >
<template #display> <template #display>
<ColorPicker v-model="color" id="icon-color" class="color-picker"/> <ColorPicker
v-model="color"
id="icon-color"
class="color-picker"
/>
</template> </template>
</InputField> </InputField>
@@ -117,7 +118,7 @@ const customizingActive = computed(() => {
<InputField <InputField
id="absolute-stroke-width" id="absolute-stroke-width"
label="Absolute Stroke width" label="Absolute stroke width"
> >
<Switch <Switch
id="absolute-stroke-width" id="absolute-stroke-width"
@@ -143,6 +144,7 @@ const customizingActive = computed(() => {
font-size: 16px; font-size: 16px;
/* margin-bottom: 12px; */ /* margin-bottom: 12px; */
} }
.customizer-card { .customizer-card {
background: var(--vp-c-bg); background: var(--vp-c-bg);
padding: 12px 24px 24px; padding: 12px 24px 24px;
@@ -151,7 +153,7 @@ const customizingActive = computed(() => {
position: relative; position: relative;
z-index: 0; z-index: 0;
border: 1px solid transparent; border: 1px solid transparent;
transition: border-color .4s ease-in-out; transition: border-color 0.4s ease-in-out;
} }
.customizer-card.customized { .customizer-card.customized {

View File

@@ -1,4 +1,4 @@
import { IconNode } from 'lucide-vue-next/src/createLucideIcon'; import { type IconNode } from 'lucide-vue-next/src/types';
import Vue from 'vue'; import Vue from 'vue';
declare module '*.vue' { declare module '*.vue' {
@@ -20,5 +20,6 @@ declare module 'node:module' {
} }
declare module '*.node.json' { declare module '*.node.json' {
export default IconNode; const value: IconNode;
export default value;
} }

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/lucide-angular), [Astro](https://lucide.dev/guide/packages/lucide-astro), and [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs). Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](https://lucide.dev/guide/packages/lucide), [React](https://lucide.dev/guide/packages/lucide-react), [React Native](https://lucide.dev/guide/packages/lucide-react-native), [Vue](https://lucide.dev/guide/packages/lucide-vue), [Vue 3](https://lucide.dev/guide/packages/lucide-vue-next), [Svelte](https://lucide.dev/guide/packages/lucide-svelte), [Preact](https://lucide.dev/guide/packages/lucide-preact), [Solid](https://lucide.dev/guide/packages/lucide-solid), [Angular](https://lucide.dev/guide/packages/angular), [Astro](https://lucide.dev/guide/packages/lucide-astro), and [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs).
## Community ## Community

View File

@@ -0,0 +1,267 @@
# `@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
1. Ensure the icon is provided via `provideLucideIcons()` if using string names
2. Check that the icon name matches exactly (case-sensitive)
3. 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`.

View File

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

@@ -9,8 +9,8 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path d="M19 12H2" /> <path d="M11 7 6 2" />
<path d="M18.992 12H2.041" />
<path d="M21.145 18.38A3.34 3.34 0 0 1 20 16.5a3.3 3.3 0 0 1-1.145 1.88c-.575.46-.855 1.02-.855 1.595A2 2 0 0 0 20 22a2 2 0 0 0 2-2.025c0-.58-.285-1.13-.855-1.595" /> <path d="M21.145 18.38A3.34 3.34 0 0 1 20 16.5a3.3 3.3 0 0 1-1.145 1.88c-.575.46-.855 1.02-.855 1.595A2 2 0 0 0 20 22a2 2 0 0 0 2-2.025c0-.58-.285-1.13-.855-1.595" />
<path d="m6 2 5 5" />
<path d="m8.5 4.5 2.148-2.148a1.205 1.205 0 0 1 1.704 0l7.296 7.296a1.205 1.205 0 0 1 0 1.704l-7.592 7.592a3.615 3.615 0 0 1-5.112 0l-3.888-3.888a3.615 3.615 0 0 1 0-5.112L5.67 7.33" /> <path d="m8.5 4.5 2.148-2.148a1.205 1.205 0 0 1 1.704 0l7.296 7.296a1.205 1.205 0 0 1 0 1.704l-7.592 7.592a3.615 3.615 0 0 1-5.112 0l-3.888-3.888a3.615 3.615 0 0 1 0-5.112L5.67 7.33" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 622 B

24
icons/stone.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"Alportan",
"karsa-mistmere"
],
"tags": [
"mineral",
"geology",
"nature",
"solid",
"pebble",
"crystal",
"ore",
"hard",
"coal",
"stone",
"rock",
"boulder"
],
"categories": [
"nature"
]
}

15
icons/stone.svg Normal file
View File

@@ -0,0 +1,15 @@
<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="M11.264 2.205A4 4 0 0 0 6.42 4.211l-4 8a4 4 0 0 0 1.359 5.117l6 4a4 4 0 0 0 4.438 0l6-4a4 4 0 0 0 1.576-4.592l-2-6a4 4 0 0 0-2.53-2.53z" />
<path d="M11.99 22 14 12l7.822 3.184" />
<path d="M14 12 8.47 2.302" />
</svg>

After

Width:  |  Height:  |  Size: 435 B

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: {},
},
],
};

View File

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

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

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

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

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

View File

@@ -0,0 +1,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
A standalone, signal based, zoneless 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/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>

View File

@@ -0,0 +1,51 @@
{
"$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

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

View File

@@ -0,0 +1,76 @@
{
"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 --useDefaultExports=0",
"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:*",
"@lucide/helpers": "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"
}
}

View File

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

@@ -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',
};

View File

@@ -0,0 +1,25 @@
import { TestBed } from '@angular/core/testing';
import { LUCIDE_CONFIG, lucideDefaultConfig, provideLucideConfig } from './lucide-config';
describe('Lucide config', () => {
describe('LUCIDE_CONFIG', () => {
it('should use default', () => {
expect(TestBed.inject(LUCIDE_CONFIG)).toBe(lucideDefaultConfig);
});
});
describe('provideLucideConfig', () => {
it('should use defaults', () => {
TestBed.configureTestingModule({
providers: [
provideLucideConfig({
size: 18,
}),
],
});
expect(TestBed.inject(LUCIDE_CONFIG)).toEqual({
...lucideDefaultConfig,
size: 18,
});
});
});
});

View File

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

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

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

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

View File

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

@@ -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,
});
});
});
});

View File

@@ -0,0 +1,64 @@
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 provideLucideConfig}
*/
export const LUCIDE_ICONS = new InjectionToken<LucideIcons>('Lucide icons', {
factory: () => ({}),
});
/**
* Provide Lucide icons by name.
*
* @remarks
* Warning! This provider will convert dictionary keys to lower-kebab-case.
*
* @param icons Either a dictionary of icons or a list of Angular icon components.
*
* @usage
* ```ts
* import { provideLucideIcons, SquareCheck } from '@lucide/angular';
* import { MyCustomIcon } from './custom-icons/circle-check';
*
* providers: [
* provideLucideIcons({
* SquareCheck,
* MyCustomIcon, // LucideIconData
* }),
* ]
* ```
*
* ```html
* <svg lucideIcon="my-custom-icon" />
* ```
*/
export function provideLucideIcons(
icons: Record<string, LucideIconData | LucideIconComponentType> | Array<LucideIconComponentType>,
): Provider {
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

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

View File

@@ -0,0 +1,44 @@
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<{
iconName: Signal<Nullable<string>>;
iconData: Signal<Nullable<LucideIconData>>;
}> & {
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

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

View File

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

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"
}
]
}

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"
]
}

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"
}
}

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"
]
}

4528
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -48,7 +48,11 @@ function generateIconFiles({
]); ]);
const getSvg = () => readSvg(`${iconName}.svg`, iconsDir); const getSvg = () => readSvg(`${iconName}.svg`, iconsDir);
const { deprecated = false, toBeRemovedInVersion = undefined } = iconMetaData[iconName]; const {
deprecated = false,
toBeRemovedInVersion = undefined,
aliases = [],
} = iconMetaData[iconName];
const deprecationReason = deprecated const deprecationReason = deprecated
? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', { ? deprecationReasonTemplate(iconMetaData[iconName]?.deprecationReason ?? '', {
componentName, componentName,
@@ -64,6 +68,7 @@ function generateIconFiles({
getSvg, getSvg,
deprecated, deprecated,
deprecationReason, deprecationReason,
aliases,
}); });
const output = pretty const output = pretty
@@ -71,7 +76,7 @@ function generateIconFiles({
singleQuote: true, singleQuote: true,
trailingComma: 'all', trailingComma: 'all',
printWidth: 100, printWidth: 100,
parser: 'babel', parser: iconFileExtension.endsWith('.ts') ? 'babel-ts' : 'babel',
}) })
: elementTemplate; : elementTemplate;
@@ -81,7 +86,7 @@ function generateIconFiles({
const output = `export { default } from "./${iconName}${iconFileExtension}";\n`; const output = `export { default } from "./${iconName}${iconFileExtension}";\n`;
const location = path.join( const location = path.join(
iconsDistDirectory, iconsDistDirectory,
`${iconName}${separateIconFileExportExtension ?? iconFileExtension}`, `${iconName}${separateIconFileExportExtension ?? iconFileExtension}`
); );
await fs.promises.writeFile(location, output, 'utf-8'); await fs.promises.writeFile(location, output, 'utf-8');

View File

@@ -31,6 +31,7 @@ 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;
} }
@@ -62,6 +63,7 @@ const {
separateIconFileExportExtension = undefined, separateIconFileExportExtension = undefined,
aliasesFileExtension = '.js', aliasesFileExtension = '.js',
aliasImportFileExtension = '', aliasImportFileExtension = '',
useDefaultExports = true,
pretty = true, pretty = true,
} = cliArguments; } = cliArguments;
@@ -125,6 +127,7 @@ async function buildIcons() {
icons, icons,
exportModuleNameCasing, exportModuleNameCasing,
importImportFileExtension, importImportFileExtension,
useDefaultExports
); );
} }

View File

@@ -6,14 +6,17 @@ 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 type TemplateFunction = (params: { export interface ExportTemplate {
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;
}) => Promise<string>; aliases?: (string | AliasDeprecation)[];
}
export type TemplateFunction = (params: ExportTemplate) => Promise<string>;
export type Path = string; export type Path = string;

View File

@@ -1,15 +1,4 @@
import { type IconNode } from '../types.ts'; import type { TemplateFunction } 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;