Compare commits

...

8 Commits

Author SHA1 Message Date
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
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
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
31 changed files with 3213 additions and 4988 deletions

View File

@@ -1,5 +1,5 @@
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 { renderToStaticMarkup } from 'react-dom/server';
import { IconContent } from './generateZip';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@
stroke-linecap="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="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" />
</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

@@ -1,6 +1,6 @@
{
"name": "lucide-angular",
"description": "A Lucide icon library package for Angular applications.",
"name": "@lucide/angular",
"description": "A Lucide icon library package for Angular applications",
"version": "0.0.1",
"author": "SMAH1",
"license": "ISC",
@@ -38,19 +38,19 @@
"version": "pnpm version --git-tag-version=false"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.11",
"@angular-eslint/builder": "~13.0.0",
"@angular-eslint/eslint-plugin": "~13.0.0",
"@angular-eslint/eslint-plugin-template": "~13.0.0",
"@angular-eslint/schematics": "~13.0.0",
"@angular-eslint/template-parser": "~13.0.0",
"@angular/cli": "~13.3.11",
"@angular/common": "~13.3.0",
"@angular/compiler": "~13.3.0",
"@angular/compiler-cli": "~13.3.0",
"@angular/core": "~13.3.0",
"@angular/platform-browser": "~13.3.0",
"@angular/platform-browser-dynamic": "~13.3.0",
"@angular-devkit/build-angular": "~17.3.14",
"@angular-eslint/builder": "~17.5.3",
"@angular-eslint/eslint-plugin": "~17.5.3",
"@angular-eslint/eslint-plugin-template": "~17.5.3",
"@angular-eslint/schematics": "~17.5.3",
"@angular-eslint/template-parser": "~17.5.3",
"@angular/cli": "~17.3.14",
"@angular/common": "~17.3.12",
"@angular/compiler": "~17.3.12",
"@angular/compiler-cli": "~17.3.12",
"@angular/core": "~17.3.12",
"@angular/platform-browser": "~17.3.12",
"@angular/platform-browser-dynamic": "~17.3.12",
"@lucide/build-icons": "workspace:*",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
@@ -65,12 +65,12 @@
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"ng-packagr": "^13.3.0",
"ng-packagr": "^17.3.0",
"prettier": "^2.8.4",
"rxjs": "~7.5.0",
"rxjs": "~6.5.3",
"ts-node": "~10.9.1",
"tslib": "^2.3.0",
"typescript": "~4.6.2",
"typescript": "~5.4.5",
"zone.js": "~0.11.4"
},
"peerDependencies": {

View File

@@ -8,26 +8,57 @@ export default defineExportTemplate(async ({
getSvg,
deprecated,
deprecationReason,
aliases = [],
toPascalCase,
}) => {
const svgContents = await getSvg();
const svgBase64 = base64SVG(svgContents);
const angularComponentName = `Lucide${componentName}Component`;
const selectors = [`svg[lucide-${iconName}]`];
const aliasExports: {[aliasComponentName: string]: boolean} = {};
for (const alias of aliases) {
const aliasName = typeof alias === 'string' ? alias : alias.name;
const aliasComponentName = `Lucide${toPascalCase(aliasName)}Component`;
const aliasSelector = `svg[lucide-${aliasName}]`;
selectors.push(aliasSelector);
if (aliasComponentName !== angularComponentName && !(aliasComponentName in aliasExports)) {
aliasExports[aliasComponentName] = true;
}
}
return `\
import { LucideIconData } from './types';
import { LucideAngularComponent } from '../lib/lucide-angular.component';
import { Component } 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-vue-next - Documentation
* @see https://lucide.dev/guide/packages/lucide-angular - Documentation
*
* @param {Object} props - Lucide icons props and any valid SVG attribute
* @returns {FunctionalComponent} Vue component
* ${deprecated ? `@deprecated ${deprecationReason}` : ''}
*/
const ${componentName}: LucideIconData = ${JSON.stringify(children)}; //eslint-disable-line no-shadow-restricted-names
@Component({
selector: '${selectors.join(', ')}',
template: '',
standalone: true,
})
export class ${angularComponentName} extends LucideAngularComponent {
override icon = ${JSON.stringify(children)} as LucideIconData;
override name = '${iconName}';
}
export default ${componentName};
${Object.entries(aliasExports).map(([aliasComponentName]) => {
return `
/**
* @deprecated
* @see ${angularComponentName}
*/
export const ${aliasComponentName} = ${angularComponentName};
`;
}).join(`\n\n`)}
`;
});

View File

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

View File

@@ -1,8 +0,0 @@
{
"ngPackage": {
"dest": "dist",
"lib": {
"entryFile": "../public-api.ts"
}
}
}

View File

@@ -1,4 +0,0 @@
/** @deprecated Use the injection token LUCIDE_ICONS instead. Will be removed in v1.0. */
export class Icons {
constructor(private icons: object) {}
}

View File

@@ -1,23 +1,19 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LucideAngularModule } from './lucide-angular.module';
import { formatFixed, LucideAngularComponent } from './lucide-angular.component';
import defaultAttributes from '../icons/constants/default-attributes';
import { LucideIcons } from '../icons/types';
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 testIcons: LucideIcons = {
Demo: [['polyline', { points: '1 1 22 22' }]],
};
const testIcon: LucideIconData = [['polyline', { points: '1 1 22 22' }]];
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LucideAngularComponent, TestHostComponent],
imports: [LucideAngularModule.pick(testIcons)],
imports: [],
}).compileComponents();
testHostFixture = TestBed.createComponent(TestHostComponent);
testHostComponent = testHostFixture.componentInstance;
@@ -63,7 +59,7 @@ describe('LucideAngularComponent', () => {
testHostComponent.setAbsoluteStrokeWidth(true);
testHostFixture.detectChanges();
expect(getSvgAttribute('stroke-width')).toBe(
formatFixed(strokeWidth / (size / defaultAttributes.height)),
formatFixed(strokeWidth / (size / defaultAttributes.height))
);
});
@@ -71,6 +67,7 @@ describe('LucideAngularComponent', () => {
selector: 'lucide-spec-host-component',
template: ` <i-lucide
name="demo"
[img]="testIcon"
class="my-icon"
[color]="color"
[size]="size"
@@ -83,6 +80,7 @@ describe('LucideAngularComponent', () => {
size?: number;
strokeWidth?: number;
absoluteStrokeWidth = true;
readonly testIcon = testIcon;
setColor(color: string): void {
this.color = color;

View File

@@ -5,12 +5,12 @@ import {
Inject,
Input,
OnChanges,
OnInit,
Renderer2,
SimpleChange,
} from '@angular/core';
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';
interface TypedChange<T> extends SimpleChange {
@@ -22,7 +22,7 @@ type SvgAttributes = { [key: string]: string | number };
type LucideAngularComponentChanges = {
name?: TypedChange<string | LucideIconData>;
img?: TypedChange<LucideIconData | undefined>;
icon?: TypedChange<LucideIconData | undefined>;
color?: TypedChange<string>;
size?: TypedChange<number>;
strokeWidth?: TypedChange<number>;
@@ -35,25 +35,27 @@ export function formatFixed(number: number, decimals = 3): string {
}
@Component({
selector: 'lucide-angular, lucide-icon, i-lucide, span-lucide',
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'svg[lucideIcon]',
template: '<ng-content></ng-content>',
standalone: true,
})
export class LucideAngularComponent implements OnChanges {
export class LucideAngularComponent implements OnInit, OnChanges {
@Input() class?: string;
@Input() name?: string | LucideIconData;
@Input() img?: LucideIconData;
@Input() name?: string;
@Input('lucideIcon') icon?: LucideIconData;
@Input() color?: string;
@Input() absoluteStrokeWidth = false;
defaultSize: number;
constructor(
@Inject(ElementRef) private elem: ElementRef,
@Inject(Renderer2) private renderer: Renderer2,
@Inject(ChangeDetectorRef) private changeDetector: ChangeDetectorRef,
@Inject(LUCIDE_ICONS) private iconProviders: LucideIconProviderInterface[],
@Inject(LucideIconConfig) private iconConfig: LucideIconConfig,
@Inject(ElementRef) protected elem: ElementRef,
@Inject(Renderer2) protected renderer: Renderer2,
@Inject(ChangeDetectorRef) protected changeDetector: ChangeDetectorRef,
@Inject(LucideIconConfig) protected iconConfig: LucideIconConfig
) {
this.defaultSize = defaultAttributes.height;
console.log(this.name, this.icon);
}
_size?: number;
@@ -84,40 +86,37 @@ export class LucideAngularComponent implements OnChanges {
}
}
ngOnInit() {
this.buildIcon();
}
ngOnChanges(changes: LucideAngularComponentChanges): void {
if (
changes.name ||
changes.img ||
changes.icon ||
changes.color ||
changes.size ||
changes.absoluteStrokeWidth ||
changes.strokeWidth ||
changes.class
) {
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;
const nameOrIcon = this.img ?? this.name;
if (typeof nameOrIcon === 'string') {
const icoOfName = this.getIcon(this.toPascalCase(nameOrIcon));
if (icoOfName) {
this.replaceElement(icoOfName);
} else {
throw new Error(
`The "${nameOrIcon}" icon has not been provided by any available icon providers.`,
);
}
} else if (Array.isArray(nameOrIcon)) {
this.replaceElement(nameOrIcon);
} else {
throw new Error(`No icon name or image has been provided.`);
}
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(this.icon);
}
}
replaceElement(img: LucideIconData): void {
const attributes = {
...defaultAttributes,
@@ -128,7 +127,10 @@ export class LucideAngularComponent implements OnChanges {
? formatFixed(this.strokeWidth / (this.size / this.defaultSize))
: this.strokeWidth.toString(10),
};
const icoElement = this.createElement(['svg', attributes, img]);
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('_', '-')}`);
@@ -138,24 +140,19 @@ export class LucideAngularComponent implements OnChanges {
...this.class
.split(/ /)
.map((a) => a.trim())
.filter((a) => a.length > 0),
.filter((a) => a.length > 0)
);
}
const childElements = this.elem.nativeElement.childNodes;
for (const child of childElements) {
for (const child of icoElement.children) {
this.renderer.removeChild(this.elem.nativeElement, child);
}
this.renderer.appendChild(this.elem.nativeElement, icoElement);
for (const node of img) {
const childElement = this.createElement(node);
this.renderer.appendChild(icoElement, childElement);
}
}
toPascalCase(str: string): string {
return str.replace(
/(\w)([a-z0-9]*)(_|-|\s*)/g,
(g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase(),
);
}
private parseNumber(value: string | number): number {
protected parseNumber(value: string | number): number {
if (typeof value === 'string') {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
@@ -166,21 +163,10 @@ export class LucideAngularComponent implements OnChanges {
return value;
}
private getIcon(name: string): LucideIconData | null {
for (const iconProvider of Array.isArray(this.iconProviders)
? this.iconProviders
: [this.iconProviders]) {
if (iconProvider.hasIcon(name)) {
return iconProvider.getIcon(name);
}
}
return null;
}
private createElement([tag, attrs, children = []]: readonly [
protected createElement([tag, attrs, children = []]: readonly [
string,
SvgAttributes,
LucideIconData?,
LucideIconData?
]) {
const element = this.renderer.createElement(tag, 'http://www.w3.org/2000/svg');

View File

@@ -1,31 +0,0 @@
import { ModuleWithProviders, NgModule, Optional } from '@angular/core';
import { LucideAngularComponent } from './lucide-angular.component';
import { LucideIcons } from '../icons/types';
import { LUCIDE_ICONS, LucideIconProvider } from './lucide-icon.provider';
import { Icons } from './icons.provider';
const legacyIconProviderFactory = (icons?: LucideIcons) => {
return new LucideIconProvider(icons ?? {});
};
@NgModule({
declarations: [LucideAngularComponent],
imports: [],
exports: [LucideAngularComponent],
})
export class LucideAngularModule {
static pick(icons: LucideIcons): ModuleWithProviders<LucideAngularModule> {
return {
ngModule: LucideAngularModule,
providers: [
{ provide: LUCIDE_ICONS, multi: true, useValue: new LucideIconProvider(icons) },
{
provide: LUCIDE_ICONS,
multi: true,
useFactory: legacyIconProviderFactory,
deps: [[new Optional(), Icons]],
},
],
};
}
}

View File

@@ -1,24 +0,0 @@
import { LucideIconData, LucideIcons } from '../icons/types';
import { InjectionToken } from '@angular/core';
export interface LucideIconProviderInterface {
hasIcon(name: string): boolean;
getIcon(name: string): LucideIconData | null;
}
export const LUCIDE_ICONS = new InjectionToken<LucideIconProviderInterface>('LucideIcons', {
factory: () => new LucideIconProvider({}),
});
export class LucideIconProvider implements LucideIconProviderInterface {
constructor(private icons: LucideIcons) {}
getIcon(name: string): LucideIconData | null {
return this.hasIcon(name) ? this.icons[name] : null;
}
hasIcon(name: string): boolean {
return typeof this.icons === 'object' && name in this.icons;
}
}

View File

@@ -1,10 +1,7 @@
import * as icons from './icons/lucide-icons';
export * from './lib/lucide-angular.component';
export * from './lib/lucide-angular.module';
export * from './lib/lucide-icon.config';
export * from './lib/lucide-icon.provider';
export * from './icons/lucide-icons';
export * from './icons/types';
export * from './aliases';
export { icons };

7361
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ export default async function generateExportFile(
} else if (exportModuleNameCasing === 'pascal') {
componentName = toPascalCase(iconName);
}
const importString = `export { default as ${componentName} } from './${iconName}${iconFileExtension}';\n`;
const importString = `export * from './${iconName}${iconFileExtension}';\n`;
return appendFile(importString, fileName, outputDirectory);
});

View File

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

View File

@@ -13,6 +13,8 @@ export type TemplateFunction = (params: {
getSvg: () => Promise<string>;
deprecated?: boolean;
deprecationReason?: string;
aliases?: (string | AliasDeprecation)[];
toPascalCase: (value: string) => string;
}) => Promise<string>;
export type Path = string;

View File

@@ -7,6 +7,8 @@ export interface ExportTemplate {
getSvg: () => Promise<string>;
deprecated: boolean;
deprecationReason: string;
aliases: Array<string | { name: string }>;
toPascalCase: (value: string) => string;
}
export type TemplateFunction = (params: ExportTemplate) => Promise<string>;