Merge branch 'main' of https://github.com/lucide-icons/lucide into algolia-search

This commit is contained in:
Eric
2026-10-19 11:42:16 +02:00
425 changed files with 14765 additions and 3336 deletions
@@ -41,7 +41,6 @@ export default eventHandler((event) => {
<Diff
oldSrc={oldSrc}
newSrc={newSrc}
showGrid
height={height}
width={width}
>
@@ -48,7 +48,6 @@ export default eventHandler((event) => {
<Diff
oldSrc={oldSrc}
newSrc={newSrc}
showGrid
height={height}
width={width}
>
+6 -5
View File
@@ -18,11 +18,12 @@ export default eventHandler((event) => {
return sendError(event, error);
}
const width = getQuery(event).width || undefined;
const height = getQuery(event).height || undefined;
const color = getQuery(event).color || undefined;
const strokeWidth = getQuery(event).strokeWidth || undefined;
const background = getQuery(event).background || undefined;
const query = getQuery<Record<string, string | undefined>>(event);
const width = query.width || undefined;
const height = query.height || undefined;
const color = query.color || undefined;
const strokeWidth = query.strokeWidth || undefined;
const background = query.background || undefined;
const LucideIcon = createLucideIcon(params.iconName, iconNode);
+8 -1
View File
@@ -15,6 +15,10 @@ const defaultSandpackCSS = await readFile(
fileURLToPath(new URL('./theme/sandpack-default.css', import.meta.url)),
'utf-8',
);
const iconStyleHeadScript = await readFile(
fileURLToPath(new URL('./theme/iconStyleHead.js', import.meta.url)),
'utf-8',
);
const title = 'Lucide';
const socialTitle = 'Lucide Icons';
@@ -90,7 +94,10 @@ export default defineConfig({
}) as unknown as UserConfig['vite']['plugins'][0],
],
},
head: getHeadConfig({ title, description, socialTitle }),
head: [
...getHeadConfig({ title, description, socialTitle }),
['script', {}, iconStyleHeadScript],
],
transformPageData,
themeConfig: {
logo: {
+1 -1
View File
@@ -101,7 +101,7 @@
},
{
"name": "notifications",
"title": "Notification"
"title": "Notifications"
},
{
"name": "people",
+20 -2
View File
@@ -85,8 +85,26 @@
}
]
},
"lucide-react-native": {
"@lucide/solid": {
"order": 5,
"icon": "solid",
"docsAlias": "solid",
"packageDirname": "solid",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/@lucide/solid",
"href": "https://www.npmjs.com/package/@lucide/solid"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/@lucide/solid",
"href": "https://www.npmjs.com/package/@lucide/solid"
}
]
},
"lucide-react-native": {
"order": 6,
"icon": "react-native",
"docsAlias": "react-native",
"shields": [
@@ -103,7 +121,7 @@
]
},
"@lucide/angular": {
"order": 6,
"order": 7,
"icon": "angular",
"docsAlias": "angular",
"packageDirname": "angular",
@@ -228,5 +228,24 @@
],
"source": "https://github.com/GyeongHoKim/lucide-lit",
"documentation": "https://github.com/GyeongHoKim/lucide-lit/blob/main/README.md"
},
{
"name": "lucide-jte",
"description": "Implementation of Lucide icons for jte (Java Template Engine).",
"icon": "/framework-logos/java.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/github/v/release/sgruendel/lucide-jte",
"href": "https://github.com/sgruendel/lucide-jte/releases"
},
{
"alt": "Maven Central",
"src": "https://img.shields.io/maven-central/v/io.github.sgruendel/lucide-jte.svg",
"href": "https://central.sonatype.com/artifact/io.github.sgruendel/lucide-jte"
}
],
"source": "https://github.com/sgruendel/lucide-jte",
"documentation": "https://github.com/sgruendel/lucide-jte/blob/main/README.md"
}
]
+1 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { type JSX } from 'react';
interface BackdropProps {
src: string;
+8 -5
View File
@@ -397,10 +397,14 @@ interface SvgPreviewProps extends React.SVGProps<SVGSVGElement> {
children?: React.ReactNode;
}
const SvgPreview = (
{ src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps,
ref,
) => {
const SvgPreview = ({
src,
children,
height = 24,
width = 24,
showGrid = false,
...props
}: SvgPreviewProps) => {
const subGridSize =
Math.max(height, width) % 3 === 0
? Math.max(height, width) > 24
@@ -413,7 +417,6 @@ const SvgPreview = (
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
+1
View File
@@ -16,6 +16,7 @@ export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
return {
name,
title: parsedFileContent.title,
description: parsedFileContent.description,
};
});
}
@@ -73,7 +73,7 @@ function hidePopup() {
:iconNode="x"
name="x"
:size="20"
absoluteStrokeWidth
nonScalingStroke
/>
</Button>
@@ -1,5 +1,4 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import { computed } from 'vue';
const props = defineProps<{
@@ -7,8 +6,6 @@ const props = defineProps<{
id: string;
}>();
const { isDark } = useData();
const emit = defineEmits(['update:modelValue']);
function limitHexInput(val: string) {
@@ -22,7 +19,7 @@ function limitHexInput(val: string) {
const value = computed({
get: () => {
if (props.modelValue == null || props.modelValue === 'currentColor') {
return isDark.value ? '#ffffff' : '#000000';
return null;
}
return props.modelValue;
@@ -27,7 +27,7 @@ defineProps({
<style scoped>
.fake-input {
background: var(--vp-c-bg-soft);
background: var(--vp-c-bg-alt);
border-radius: 8px;
color: var(--vp-c-text-2);
padding: 11px 16px;
@@ -5,7 +5,7 @@ import Button from './Button.vue';
</script>
<template>
<Button class="reset-button">
<Button class="reset-button" aria-label="Reset style">
<Icon
:size="20"
:iconNode="rotateCw"
@@ -16,6 +16,7 @@ import Button from './Button.vue';
<style scoped>
.reset-button {
transition: ease-in-out 0.1s opacity;
background: none;
padding: 0;
}
@@ -29,6 +30,12 @@ import Button from './Button.vue';
border-color: transparent;
}
.reset-button:disabled {
cursor: default;
pointer-events: none;
opacity: 0.5;
}
/* a rotate css animation keyframes */
@keyframes rotate {
0% {
@@ -1,22 +1,33 @@
<script setup>
import { Switch } from '@headlessui/vue'
import { onMounted, ref } from 'vue'
defineProps({
modelValue: {
type: Boolean,
default: false
},
id: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const isHydrated = ref(false)
onMounted(() => {
isHydrated.value = true
})
</script>
<template>
<Switch
:model-value="modelValue"
:model-value="isHydrated ? modelValue : false"
@update:model-value="emit('update:modelValue', $event)"
class="switch"
:class="{ enabled: modelValue }"
:class="{ enabled: isHydrated && modelValue }"
:id="id"
>
<span class="thumb" />
</Switch>
@@ -1,13 +1,11 @@
<script lang="ts" setup>
import { type FunctionalComponent } from 'vue';
import { type LucideProps } from '@lucide/vue/src/types';
import { Sandpack } from 'sandpack-vue3';
import { Sandpack, type SandpackProps } from 'sandpack-vue3';
defineProps<FunctionalComponent<LucideProps>>();
const props = defineProps<SandpackProps>();
</script>
<template>
<Sandpack />
<Sandpack v-bind="props" />
</template>
<style>
@@ -1,7 +1,6 @@
<script setup lang="tsx">
import VPSidebarGroup from 'vitepress/dist/client/theme-default/components/VPSidebarGroup.vue';
import VPNavBarSearch from '../../overrides/VPNavBarSearch.vue';
import sidebar, { guideSidebarTop } from '../../../sidebar';
import sidebar from '../../../sidebar';
import { useData, useRouter } from 'vitepress';
import Select from '../base/Select.vue';
import { computed } from 'vue';
@@ -75,15 +74,7 @@ function onSelectFramework(item: Framework) {
</script>
<template>
<VPNavBarSearch v-if="page?.relativePath?.startsWith?.('guide')"/>
<VPSidebarGroup
:items="guideSidebarTop"
v-if="page?.relativePath?.startsWith?.('guide')"
/>
<div
class="framework-select"
v-if="page?.relativePath?.startsWith?.('guide')"
>
<div class="framework-select">
<label for="framework-select">Framework</label>
<Select
id="framework-select"
@@ -94,10 +85,7 @@ function onSelectFramework(item: Framework) {
</div>
<VPSidebarGroup
:key="selected.value"
v-if="
page?.relativePath?.startsWith?.('guide') &&
!page?.relativePath?.startsWith?.(selected.value.substring(1))
"
v-if="!page?.relativePath?.startsWith?.(selected.value.substring(1))"
:items="sidebar[selected.value]"
/>
</template>
@@ -56,7 +56,6 @@ const scaleDownVariants: Variants = {
riseUp: {
x: 0.5,
y: -0.5,
animationName: 'riseUp',
scale: 1,
transition: {
delay: 0.5,
@@ -68,7 +67,6 @@ const scaleDownVariants: Variants = {
x: -10.5,
y: -10.5,
scale: 0.1,
animationName: 'small',
transition: {
delay: 1,
duration: 1,
@@ -84,7 +82,6 @@ const drawAnimation = ref('visible');
const draw: Variants = {
hidden: { pathLength: 0, opacity: 0 },
visible: {
animationName: 'visible',
pathLength: 1.02,
opacity: 1,
transition: {
@@ -93,7 +90,6 @@ const draw: Variants = {
},
},
exit: (path) => ({
animationName: 'exit',
stroke: path ? 'var(--vp-c-text-1)' : 'var(--vp-c-brand)',
pathLength: 1.02,
opacity: 1,
@@ -103,25 +99,26 @@ const draw: Variants = {
}),
};
const onAnimationComplete = (item) => {
if (item.animationName === 'visible') {
// motion-v passes the variant label that finished, not the resolved variant
const onAnimationComplete = (variant: unknown) => {
if (variant === 'visible') {
drawAnimation.value = 'exit';
return;
}
if (item.animationName === 'exit') {
if (variant === 'exit') {
showHandles.value = false;
scaleDownAnimation.value = 'small';
}
if (item.animationName === 'small') {
if (variant === 'small') {
iconGridAnimation.value = 'showIcons';
}
if (item.animationName === 'riseUp') {
if (variant === 'riseUp') {
scaleDownAnimation.value = 'small';
}
if (item.animationName === 'showIcons') {
if (variant === 'showIcons') {
shrinkIconAnimation.value = 'shrinkIcons';
}
if (item.animationName === 'shrinkIcons') {
if (variant === 'shrinkIcons') {
iconGridAnimation.value = 'initial';
setTimeout(() => {
@@ -134,14 +131,12 @@ const randomIndex = ref(Math.floor(Math.random() * 64));
const iconAnimationVariants: Variants = {
initial: {
animationName: 'end',
opacity: 0,
x: 0,
y: 0,
transition: { duration: 1, delay: 1, ease: 'easeInOut' },
},
showIcons: (index) => ({
animationName: 'showIcons',
opacity: [0, 1, 1],
x: [0.5, 0, 0],
y: [-0.5, 0, 0],
@@ -155,7 +150,6 @@ const shrinkIconAnimation = ref('initial');
const shrinkIconVariants: Variants = {
initial: { strokeWidth: 2 },
shrinkIcons: (index) => ({
animationName: 'shrinkIcons',
opacity: 1,
strokeWidth: 0,
transition: { delay: 1.8, duration: 1.5, ease: 'easeInOut' },
@@ -257,7 +251,7 @@ const shrinkIconVariants: Variants = {
</motion.g>
<motion.g
class="svg-preview-control-path-marker-group"
stroke="#fff"
:style="{ stroke: 'var(--vp-c-text-1)' }"
stroke-width="0.125"
:initial="{ opacity: 1 }"
:animate="showHandles ? { opacity: 1 } : { opacity: 0 }"
@@ -38,7 +38,7 @@ const pickIcons = [
'map-pinned',
'megaphone',
'moon',
'music',
'sixteenth-notes',
'phone',
'power-off',
'radio',
@@ -11,18 +11,6 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
<Badge :href="`https://github.com/lucide-icons/lucide/releases/tag/${data.version}`">
v{{ data.version }}
</Badge>
<Badge
class="badge-special confetti-button animate"
href="/guide/version-1"
>
<Icon :iconNode="RocketDuotoneIcon" name="rocket-duotone" />
Version 1
<Icon
:iconNode="moveRight"
name="move-right"
class=""
/>
</Badge>
</div>
</template>
@@ -45,22 +33,6 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
gap: 8px;
padding: 4px 8px;
}
.badge-special {
font-weight: 500;
color: var(--vp-c-brand-1);
text-underline-offset: 2px;
align-items: center;
}
.badge-special::before,
.badge-special::after {
animation-delay: 0.5s !important;
animation-duration: 5s !important;
animation-iteration-count: infinite !important;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1) !important;
transform: translateX(32px);
}
</style>
<style>
@@ -1,59 +1,21 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { syncRef, useCssVar } from '@vueuse/core'
import HomeContainer from './HomeContainer.vue'
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 HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue'
import Switch from '../base/Switch.vue'
import { computed } from 'vue';
import { useIconStyle } from '../../composables/useIconStyle';
import HomeContainer from './HomeContainer.vue';
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 HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue';
import Switch from '../base/Switch.vue';
const { color, strokeWidth, size, nonScalingStroke, isCustomized, resetStyle } = useIconStyle();
const iconContainer = ref<HTMLElement | null>()
const color = ref('currentColor')
const strokeWidth = ref(2)
const size = ref(24)
const absoluteStrokeWidth = ref(false)
const colorCssVar = useCssVar(
'--customize-color',
iconContainer,
{
initialValue: 'default'
}
)
const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
iconContainer,
{
initialValue: '2'
}
)
const sizeCssVar = useCssVar(
'--customize-size',
iconContainer,
{
initialValue: '24'
}
)
syncRef(color, colorCssVar)
syncRef(strokeWidth, strokeWidthCssVar, { transform: { ltr: String, rtl: Number } })
syncRef(size, sizeCssVar, { transform: { ltr: String, rtl: Number } })
function resetStyle () {
color.value = 'currentColor'
strokeWidth.value = 2
size.value = 24
absoluteStrokeWidth.value = false
}
watch(absoluteStrokeWidth, (enabled) => {
iconContainer.value?.classList.toggle('absolute-stroke-width', enabled)
})
const iconStyle = computed(() => ({
'--home-icon-color': color.value,
'--home-icon-stroke-width': String(strokeWidth.value),
'--home-icon-size': String(size.value),
}));
</script>
<template>
@@ -62,7 +24,10 @@ watch(absoluteStrokeWidth, (enabled) => {
<div class="card-column">
<h2 class="title">
Style as you please
<ResetButton @click="resetStyle"></ResetButton>
<ResetButton
:disabled="!isCustomized"
@click="resetStyle"
/>
</h2>
<p class="copy">
Lucide has a lot of customization options to match the icons with your UI.
@@ -78,7 +43,10 @@ watch(absoluteStrokeWidth, (enabled) => {
class="color-picker-field"
>
<template #display>
<ColorPicker v-model="color" id="icon-color" />
<ColorPicker
v-model="color"
id="icon-color-picker"
/>
</template>
</InputField>
@@ -90,7 +58,7 @@ watch(absoluteStrokeWidth, (enabled) => {
<span class="customize-label">{{ strokeWidth }}px</span>
</template>
<RangeSlider
id="stroke-width"
id="stroke-width-slider"
name="stroke-width"
v-model="strokeWidth"
:min="1"
@@ -107,7 +75,7 @@ watch(absoluteStrokeWidth, (enabled) => {
<span class="customize-label">{{ size }}px</span>
</template>
<RangeSlider
id="size"
id="size-slider"
name="size"
v-model="size"
:min="16"
@@ -117,22 +85,25 @@ watch(absoluteStrokeWidth, (enabled) => {
</InputField>
<InputField
id="absolute-stroke-width"
label="Absolute Stroke width"
id="non-scaling-stroke"
label="Non-scaling stroke"
>
<template #display>
<Switch
id="absolute-stroke-width"
name="absolute-stroke-width"
v-model="absoluteStrokeWidth"
id="non-scaling-stroke-switch"
name="non-scaling-stroke"
v-model="nonScalingStroke"
/>
</template>
</InputField>
</div>
</div>
<div class="icons-container card-column" ref="iconContainer">
<HomeIconCustomizerIcons />
<div class="icons-container card-column">
<HomeIconCustomizerIcons
:class="{ 'non-scaling-stroke': nonScalingStroke }"
:style="iconStyle"
/>
</div>
</div>
</HomeContainer>
@@ -179,10 +150,6 @@ watch(absoluteStrokeWidth, (enabled) => {
display: grid;
grid-template-columns: 8fr 10fr;
}
/*
.card-column {
flex: 1;
} */
}
@media (min-width: 960px) {
@@ -191,7 +158,7 @@ watch(absoluteStrokeWidth, (enabled) => {
}
}
.color-picker-field:deep(.display-value) {
width: 138px;
.color-picker-field:deep(.icon-color-picker-input) {
width: 116px;
}
</style>
@@ -1,29 +1,35 @@
<script setup lang="ts">
import { ref } from 'vue';
import { data } from './HomeHeroIconsCard.data'
import LucideIcon from '../base/LucideIcon.vue'
import { vIntersectionObserver } from '@vueuse/components'
import { data } from './HomeHeroIconsCard.data';
import LucideIcon from '../base/LucideIcon.vue';
import { vIntersectionObserver } from '@vueuse/components';
const getInitialItems = () => data.icons.slice(0, 64)
const items = ref(getInitialItems())
const showIcons = ref(false)
const getInitialItems = () => data.icons.slice(0, 64);
const items = ref(getInitialItems());
const showIcons = ref(false);
// Added intersection observer to improve performance
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
if (isIntersecting) {
showIcons.value = true
showIcons.value = true;
}
}
};
</script>
<template>
<div class="icon-grid" v-intersection-observer="onIntersectionObserver">
<div
class="icon-grid"
v-intersection-observer="onIntersectionObserver"
>
<template v-if="showIcons">
<div
v-for="icon in items"
class="icon-grid-item"
>
<LucideIcon v-bind="icon" class="lucide-icon"/>
>
<LucideIcon
v-bind="icon"
class="lucide-icon"
/>
</div>
</template>
</div>
@@ -36,7 +42,7 @@ const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
grid-template-rows: repeat(auto-fill, minmax(68px, 1fr));
width: 100%;
height:100%;
height: 100%;
max-height: 360px;
gap: 1px;
overflow: hidden;
@@ -65,13 +71,15 @@ const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting
.lucide-icon {
will-change: width, height, stroke-width, stroke;
color: var(--customize-color, currentColor);
stroke-width: var(--customize-strokeWidth, 2);
width: calc(var(--customize-size, 24) * 1px);
height: calc(var(--customize-size, 24) * 1px);
color: var(--home-icon-color, currentColor);
stroke-width: var(--home-icon-stroke-width, 2);
width: calc(var(--home-icon-size, 24) * 1px);
height: calc(var(--home-icon-size, 24) * 1px);
max-width: 3rem;
max-height: 3rem;
}
.icons-container.absolute-stroke-width .lucide-icon {
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
.icon-grid.non-scaling-stroke .lucide-icon {
stroke-width: calc(var(--home-icon-stroke-width, 2) * 24 / var(--home-icon-size, 24));
}
</style>
@@ -48,7 +48,7 @@ onMounted(() => {
:iconNode="x"
name="x"
:size="20"
absoluteStrokeWidth
nonScalingStroke
/>
</Button>
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
@@ -9,7 +9,7 @@ const props = defineProps<{
name: string;
popoverPosition?: 'top' | 'bottom';
}>();
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
const { size, color, strokeWidth, nonScalingStroke } = useIconStyleContext();
const { animate, confetti } = useConfetti();
const componentName = computed(() => {
return (toPascalCase(props.name) as string).replace(/\s/g, '');
@@ -30,8 +30,8 @@ function copyJSX() {
attrs.push(`strokeWidth={${strokeWidth.value}}`);
}
if (absoluteStrokeWidth.value) {
attrs.push(`absoluteStrokeWidth`);
if (nonScalingStroke.value) {
attrs.push(`nonScalingStroke`);
}
const code = `<${componentName.value}${attrs.join(' ')} />`;
@@ -60,8 +60,8 @@ function copyVue() {
attrs.push(`:stroke-width="${strokeWidth.value}"`);
}
if (absoluteStrokeWidth.value) {
attrs.push(`absoluteStrokeWidth`);
if (nonScalingStroke.value) {
attrs.push(`nonScalingStroke`);
}
const code = `<${componentName.value}${attrs.join(' ')} />`;
@@ -86,8 +86,8 @@ function copyAngular() {
attrs.push(`[strokeWidth]="${strokeWidth.value}"`);
}
if (absoluteStrokeWidth.value) {
attrs.push(`[absoluteStrokeWidth]="true"`);
if (nonScalingStroke.value) {
attrs.push(`[nonScalingStroke]="true"`);
}
const code = `<lucide-icon${attrs.join(' ')}></lucide-icon>`;
@@ -204,7 +204,7 @@ const DiamondIcon = createLucideIcon('Diamond', diamond);
max-height: 3rem;
}
html.absolute-stroke-width .lucide-icon.customizable {
html.non-scaling-stroke .lucide-icon.customizable {
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
}
</style>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { IconEntity } from '../../types';
import { computed, ref } from 'vue';
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
import Icon from '@lucide/vue/src/Icon';
import { useIconStyleContext } from '../../composables/useIconStyle';
const props = defineProps<{
@@ -12,25 +12,23 @@ const props = defineProps<{
const activeSize = ref('1000%');
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
const { size, color, strokeWidth, nonScalingStroke } = useIconStyleContext();
const previewIcon = ref();
const gridLines = computed(() => Array.from({ length: size.value - 1 }));
const iconComponent = computed(() => {
if (!props.name || !props.iconNode) return null;
return createLucideIcon(props.name, props.iconNode);
});
</script>
<template>
<div class="icon-container">
<component
<Icon
v-if="name && iconNode"
:name="name"
:iconNode="iconNode"
ref="previewIcon"
:is="iconComponent"
:size="size"
:color="color"
:strokeWidth="absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth"
:strokeWidth="strokeWidth"
:nonScalingStroke="nonScalingStroke"
class="preview-icon"
:data-size="activeSize"
/>
@@ -71,7 +71,7 @@ const Icon = createLucideIcon(props.name, props.iconNode);
height: calc(var(--customize-size, 24) * 1px); */
}
html.absolute-stroke-width .lucide-icon {
html.non-scaling-stroke .lucide-icon {
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
}
</style>
@@ -1,73 +1,33 @@
<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 { computed } from 'vue';
import { useMounted } from '@vueuse/core';
import { 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>;
}>();
const { color, strokeWidth, size, nonScalingStroke, isCustomized, resetStyle } =
useIconStyleContext();
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 strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
props.rootEl?.value ?? documentRef.value,
{
initialValue: `${STYLE_DEFAULTS.strokeWidth}`,
},
);
const sizeCssVar = useCssVar('--customize-size', props.rootEl?.value ?? documentRef.value, {
initialValue: `${STYLE_DEFAULTS.size}`,
});
syncRef(color, colorCssVar, { direction: 'ltr' });
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr', transform: { ltr: String } });
syncRef(size, sizeCssVar, { direction: 'ltr', transform: { ltr: String } });
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;
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
);
});
// The server always renders the default style. Keep the first client render identical,
// otherwise the hydrated DOM keeps the stale `disabled` attribute on the reset button.
const isMounted = useMounted();
const showCustomized = computed(() => isMounted.value && isCustomized.value);
</script>
<template>
<div
class="customizer-card"
:class="{ customized: customizingActive }"
:class="{ customized: showCustomized }"
>
<div class="card-header">
<h2 class="card-title">Customizer</h2>
<ResetButton @click="resetStyle"></ResetButton>
<ResetButton
:disabled="!showCustomized"
@click="resetStyle"
/>
</div>
<InputField
id="icon-color"
@@ -75,7 +35,7 @@ const customizingActive = computed(() => {
>
<ColorPicker
v-model="color"
id="icon-color"
id="icon-color-picker"
class="color-picker"
/>
</InputField>
@@ -88,7 +48,7 @@ const customizingActive = computed(() => {
<span class="customize-label">{{ strokeWidth }}px</span>
</template>
<RangeSlider
id="stroke-width"
id="stroke-width-slider"
name="stroke-width"
v-model="strokeWidth"
:min="0.5"
@@ -105,7 +65,7 @@ const customizingActive = computed(() => {
<span class="customize-label">{{ size }}px</span>
</template>
<RangeSlider
id="size"
id="size-slider"
name="size"
v-model="size"
:min="16"
@@ -115,13 +75,13 @@ const customizingActive = computed(() => {
</InputField>
<InputField
id="absolute-stroke-width"
label="Absolute stroke width"
id="non-scaling-stroke"
label="Non-scaling stroke"
>
<Switch
id="absolute-stroke-width"
name="absolute-stroke-width"
v-model="absoluteStrokeWidth"
id="non-scaling-stroke-switch"
name="non-scaling-stroke"
v-model="nonScalingStroke"
/>
</InputField>
</div>
@@ -140,7 +100,6 @@ const customizingActive = computed(() => {
color: var(--vp-c-text-1);
line-height: 32px;
font-size: 16px;
/* margin-bottom: 12px; */
}
.customizer-card {
@@ -10,7 +10,7 @@ import {
sun,
moon,
camera,
music,
sixteenthNotes,
video,
globe,
layers,
@@ -44,7 +44,7 @@ import createElement from 'lucide/src/createElement';
import { IconNode } from 'lucide/src/types';
const icons: Component[] = [
heart, star, zap, code, feather, cloud, sun, moon, camera, music,
heart, star, zap, code, feather, cloud, sun, moon, camera, sixteenthNotes,
video, globe, layers, packageIcon, compass, command, terminal, database,
server, cpu, lock, key, shield, wifi, download, upload, search,
settings, users, mail, bell, calendar, clock, gitBranch, funnel,
@@ -8,20 +8,26 @@ const loaded = ref(false)
</script>
<script lang="ts" setup>
import '@docsearch/css'
import '@docsearch/css/dist/style.css'
import {
computed,
defineAsyncComponent,
onMounted,
onUnmounted,
} from 'vue'
import type { DefaultTheme } from 'vitepress/theme'
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
import { useData } from 'vitepress'
import FakeInput from '../base/FakeInput.vue'
const VPAlgoliaSearchBox = defineAsyncComponent(() => import('vitepress/dist/client/theme-default/components/VPAlgoliaSearchBox.vue'));
const { theme, page } = useData()
const { theme } = useData<DefaultTheme.Config>()
const algolia = computed(
() =>
(theme.value.search?.options ?? theme.value.algolia) as DefaultTheme.AlgoliaSearchOptions,
)
// whether this instance is the one rendering the (hidden) search box
const isOwner = ref(false)
@@ -39,10 +45,7 @@ const preconnect = () => {
const preconnect = document.createElement('link')
preconnect.id = id
preconnect.rel = 'preconnect'
preconnect.href = `https://${
((theme.value.search?.options as DefaultTheme.AlgoliaSearchOptions) ??
theme.value.algolia)!.appId
}-dsn.algolia.net`
preconnect.href = `https://${algolia.value.appId}-dsn.algolia.net`
preconnect.crossOrigin = ''
document.head.appendChild(preconnect)
})
@@ -126,7 +129,7 @@ function isEditingContent(event: KeyboardEvent): boolean {
<!-- docsearch renders its own button here; we only need its modal -->
<div v-if="isOwner" class="docsearch-host">
<VPAlgoliaSearchBox :algolia="theme.search?.options ?? theme.algolia" />
<VPAlgoliaSearchBox :algolia="algolia" />
</div>
</div>
</template>
@@ -0,0 +1,113 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { nextTick } from 'vue';
const storedValues = new Map<string, string>([
['icon-size', '240'],
['icon-stroke-width', '1.5'],
['icon-color', '#ff0000'],
['icon-non-scaling-stroke', 'true'],
]);
const styleProperties = new Map<string, string>();
const classes = new Set<string>();
class StorageMock {
getItem(key: string) {
return storedValues.get(key) ?? null;
}
setItem(key: string, value: string) {
storedValues.set(key, value);
}
removeItem(key: string) {
storedValues.delete(key);
}
}
class StorageEventMock {
type: string;
options: unknown;
constructor(type: string, options: unknown) {
this.type = type;
this.options = options;
}
}
const localStorage = new StorageMock();
const document = {
documentElement: {
style: { setProperty: (key: string, value: string) => styleProperties.set(key, value) },
classList: {
toggle: (value: string, enabled: boolean) =>
enabled ? classes.add(value) : classes.delete(value),
},
},
};
const window = {
document,
localStorage,
addEventListener() {},
removeEventListener() {},
dispatchEvent() {},
};
Object.assign(globalThis, {
window,
document,
localStorage,
Storage: StorageMock,
StorageEvent: StorageEventMock,
CSS: { supports: () => true },
});
const { useIconStyle, usePersistedIconStyle } = await import('./useIconStyle.ts');
test('creates a non-persisted icon style from the defaults', () => {
const style = useIconStyle();
assert.equal(style.size.value, 24);
assert.equal(style.strokeWidth.value, 2);
assert.equal(style.color.value, 'currentColor');
assert.equal(style.nonScalingStroke.value, false);
style.size.value = 48;
assert.equal(storedValues.get('icon-size'), '240');
assert.equal(styleProperties.get('--customize-size'), '240');
});
test('restores persisted settings and applies the shared CSS variables', () => {
const style = usePersistedIconStyle();
assert.equal(style.size.value, 240);
assert.equal(style.strokeWidth.value, 1.5);
assert.equal(style.color.value, '#ff0000');
assert.equal(style.nonScalingStroke.value, true);
assert.equal(style.isCustomized.value, true);
assert.equal(styleProperties.get('--customize-size'), '240');
assert.equal(styleProperties.get('--customize-strokeWidth'), '1.5');
assert.equal(styleProperties.get('--customize-color'), '#ff0000');
assert.ok(classes.has('non-scaling-stroke'));
});
test('resetting updates refs, CSS variables, and persisted values', async () => {
const style = usePersistedIconStyle();
style.resetStyle();
await nextTick();
assert.equal(style.size.value, 24);
assert.equal(style.strokeWidth.value, 2);
assert.equal(style.color.value, 'currentColor');
assert.equal(style.nonScalingStroke.value, false);
assert.equal(style.isCustomized.value, false);
assert.equal(styleProperties.get('--customize-size'), '24');
assert.equal(styleProperties.get('--customize-strokeWidth'), '2');
assert.equal(styleProperties.get('--customize-color'), 'currentColor');
assert.equal(classes.has('non-scaling-stroke'), false);
assert.equal(storedValues.get('icon-size'), '24');
assert.equal(storedValues.get('icon-stroke-width'), '2');
assert.equal(storedValues.get('icon-color'), 'currentColor');
assert.equal(storedValues.get('icon-non-scaling-stroke'), 'false');
});
@@ -1,30 +1,100 @@
import { ref, inject, Ref } from 'vue';
import { computed, inject, ref, watch, type ComputedRef, type Ref } from 'vue';
import { useLocalStorage } from '@vueuse/core';
export const ICON_STYLE_CONTEXT = Symbol('style');
interface IconSizeContext {
interface IconStyleContext {
size: Ref<number>;
strokeWidth: Ref<number>;
color: Ref<string>;
absoluteStrokeWidth: Ref<boolean>;
nonScalingStroke: Ref<boolean>;
isCustomized: ComputedRef<boolean>;
resetStyle: () => void;
}
type IconStyle = Pick<IconStyleContext, 'size' | 'strokeWidth' | 'color' | 'nonScalingStroke'>;
export const STYLE_DEFAULTS = {
size: 24,
strokeWidth: 2,
color: 'currentColor',
absoluteStrokeWidth: false,
nonScalingStroke: false,
};
export const iconStyleContext = {
size: ref(24),
strokeWidth: ref(2),
color: ref('currentColor'),
absoluteStrokeWidth: ref(false),
const persistedIconStyle = {
size: useLocalStorage('icon-size', STYLE_DEFAULTS.size, { initOnMounted: false }),
strokeWidth: useLocalStorage('icon-stroke-width', STYLE_DEFAULTS.strokeWidth, {
initOnMounted: false,
}),
color: useLocalStorage('icon-color', STYLE_DEFAULTS.color, { initOnMounted: false }),
nonScalingStroke: useLocalStorage('icon-non-scaling-stroke', STYLE_DEFAULTS.nonScalingStroke, {
initOnMounted: false,
}),
};
export function useIconStyleContext(): IconSizeContext {
const context = inject<IconSizeContext>(ICON_STYLE_CONTEXT);
function createIconStyleContext(style: IconStyle): IconStyleContext {
const isCustomized = computed(() => {
return (
style.color.value !== STYLE_DEFAULTS.color ||
style.strokeWidth.value !== STYLE_DEFAULTS.strokeWidth ||
style.size.value !== STYLE_DEFAULTS.size ||
style.nonScalingStroke.value !== STYLE_DEFAULTS.nonScalingStroke
);
});
function resetStyle() {
style.color.value = STYLE_DEFAULTS.color;
style.strokeWidth.value = STYLE_DEFAULTS.strokeWidth;
style.size.value = STYLE_DEFAULTS.size;
style.nonScalingStroke.value = STYLE_DEFAULTS.nonScalingStroke;
}
return {
...style,
isCustomized,
resetStyle,
};
}
export const iconStyleContext = createIconStyleContext(persistedIconStyle);
export function useIconStyle() {
return createIconStyleContext({
size: ref(STYLE_DEFAULTS.size),
strokeWidth: ref(STYLE_DEFAULTS.strokeWidth),
color: ref(STYLE_DEFAULTS.color),
nonScalingStroke: ref(STYLE_DEFAULTS.nonScalingStroke),
});
}
export function usePersistedIconStyle() {
return iconStyleContext;
}
if (typeof document !== 'undefined') {
watch(
[
persistedIconStyle.size,
persistedIconStyle.strokeWidth,
persistedIconStyle.color,
persistedIconStyle.nonScalingStroke,
],
([size, strokeWidth, color, nonScalingStroke]) => {
const root = document.documentElement;
root.style.setProperty('--customize-size', String(Math.min(256, Math.max(16, size))));
root.style.setProperty(
'--customize-strokeWidth',
String(Math.min(3, Math.max(0.5, strokeWidth))),
);
if (CSS.supports('color', color)) root.style.setProperty('--customize-color', color);
root.classList.toggle('non-scaling-stroke', nonScalingStroke);
},
{ immediate: true, flush: 'sync' },
);
}
export function useIconStyleContext(): IconStyleContext {
const context = inject<IconStyleContext>(ICON_STYLE_CONTEXT);
if (!context) {
throw new Error('useIconStyleContext must be used with useIconStyleProvider');
+24
View File
@@ -0,0 +1,24 @@
// Run before the body is painted: the generated HTML cannot know local preferences.
(() => {
try {
const root = globalThis.document.documentElement;
const size = globalThis.localStorage.getItem('icon-size');
const stroke = globalThis.localStorage.getItem('icon-stroke-width');
const color = globalThis.localStorage.getItem('icon-color');
const nonScaling = globalThis.localStorage.getItem('icon-non-scaling-stroke');
if ([size, stroke, color, nonScaling].every((value) => value === null)) return;
if (size !== null && Number.isFinite(Number(size))) {
root.style.setProperty('--customize-size', String(Math.min(256, Math.max(16, Number(size)))));
}
if (stroke !== null && Number.isFinite(Number(stroke)))
root.style.setProperty(
'--customize-strokeWidth',
String(Math.min(3, Math.max(0.5, Number(stroke)))),
);
if (color !== null && globalThis.CSS.supports('color', color))
root.style.setProperty('--customize-color', color);
root.classList.toggle('non-scaling-stroke', nonScaling === 'true');
} catch {
// Storage can be unavailable, for example in private browsing mode.
}
})();
+2 -2
View File
@@ -10,14 +10,14 @@ import HomeHeroInfoBefore from './components/home/HomeHeroInfoBefore.vue';
import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle';
import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView';
import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs';
import FrameworkSelect from './components/guide/FrameworkSelect.vue';
import SideBarNavBefore from './layouts/SideBarNavBefore.vue';
import SnackPlayer from './components/editors/SnackPlayer.vue';
const theme: Partial<Theme> = {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
'sidebar-nav-before': () => h(FrameworkSelect),
'sidebar-nav-before': () => h(SideBarNavBefore),
'home-hero-info-before': () => h(HomeHeroInfoBefore),
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
'home-hero-image': () => h(HomeHeroIconsCard),
@@ -0,0 +1,20 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useData } from 'vitepress';
import VPSidebarGroup from 'vitepress/dist/client/theme-default/components/VPSidebarGroup.vue';
import VPNavBarSearch from '../components/overrides/VPNavBarSearch.vue';
import FrameworkSelect from '../components/guide/FrameworkSelect.vue';
import { guideSidebarTop } from '../../sidebar';
const { page } = useData();
const isGuide = computed(() => page.value?.relativePath?.startsWith?.('guide'));
</script>
<template>
<template v-if="isGuide">
<VPNavBarSearch />
<VPSidebarGroup :items="guideSidebarTop" />
<FrameworkSelect />
</template>
</template>
+1
View File
@@ -32,6 +32,7 @@ export interface IconEntity extends IconMetaData {
export interface Category {
name: string;
title: string;
description: string;
icon?: string;
iconCount: number;
icons?: IconEntity[];
+1 -1
View File
@@ -15,7 +15,7 @@ In an effort to expand the icon set while remaining true to the original minimal
### Why should I choose Lucide over Feather Icons?
- Lucide has expanded its icon set by 500+ in the last few years. Lucide now has over 1000 icons, while Feather has around 287 icons.
- Lucide has expanded its icon set by 1600+ in the last few years. Lucide now has over 1800 icons, while Feather has around 287 icons.
- Well maintained code base.
- Active community.
+1
View File
@@ -153,6 +153,7 @@ deno add lucide-solid
```
:::
> `@lucide/solid` is for Solid 2, for Solid 1 use the `lucide-solid` package. Solid 2 is still a release candidate.
For more details, see the [documentation](./solid/index.md).
+1 -1
View File
@@ -18,7 +18,7 @@ import {
} from "lucide-solid";
```
### Turn off autocomplete in your IDE
## Turn off autocomplete in your IDE
```json [.vscode/settings.json]
{
+4 -4
View File
@@ -6,7 +6,7 @@ description: Learn about the different types exported by the `lucide-solid` pack
# TypeScript Support
List of exported types from the `lucide-solid` package.
These can be used to type your components when using Lucide icons in a TypeScript React project
These can be used to type your components when using Lucide icons in a TypeScript Solid project.
## `LucideProps`
@@ -61,10 +61,10 @@ interface ButtonProps {
label: string;
}
const IconButton = ({ icon: Icon, label }) => {
const IconButton = (props: ButtonProps) => {
return (
<button aria-label={label}>
<Icon size={16} />
<button aria-label={props.label}>
<props.icon size={16} />
</button>
);
};
+46
View File
@@ -36,6 +36,52 @@ bun add lucide-solid
:::
:::: info Using Solid 2?
Lucide ships one package per major Solid version. This guide covers **Solid 1** and the `lucide-solid` package. If your app is on Solid 2, install `@lucide/solid` instead:
::: code-group
```sh [pnpm]
pnpm add @lucide/solid
```
```sh [yarn]
yarn add @lucide/solid
```
```sh [npm]
npm install @lucide/solid
```
```sh [bun]
bun add @lucide/solid
```
:::
Both packages export the same icon components with the same props, so the rest of this guide applies unchanged — only the import specifier differs:
```jsx
import { Camera } from '@lucide/solid';
```
What does differ is Solid itself. The changes most likely to affect the code you write around Lucide:
| | `lucide-solid` | `@lucide/solid` |
| -------------------- | ----------------------------- | ----------------------- |
| Solid version | `^1.4.7` | `^2.0.0-rc.0` |
| DOM runtime package | `solid-js/web` | `@solidjs/web` |
| `jsxImportSource` | `solid-js` | `@solidjs/web` |
| Splitting icon props | `splitProps(props, ['size'])` | `omit(props, 'size')` |
| Providing context | `<Ctx.Provider value={…}>` | `<Ctx value={…}>` |
See the [Solid 2 migration guide](https://github.com/solidjs/solid/blob/next/documentation/solid-2.0/MIGRATION.md) for the full list.
Solid 2 is still a release candidate, so treat `@lucide/solid` as pre-release too. The live examples throughout this guide run on Solid 1.
::::
## Importing your first icon
Lucide is built with ES Modules, so it's completely tree-shakable.
+1 -1
View File
@@ -25,7 +25,7 @@ This is useful if you want all icons to share the same size, color, or stroke wi
You can call `setLucideProps` in your main entry file or in a top-level component to set the default properties for all icons.
```js
import { setLucideProps } from '@lucide/svelte';
import { setLucideProps } from '@lucide/svelte/context';
setLucideProps({
size: 32,
+8 -5
View File
@@ -30,10 +30,13 @@
"author": "Eric Fennis",
"license": "ISC",
"devDependencies": {
"@docsearch/js": "3.8.2",
"@lucide/build-icons": "workspace:*",
"@lucide/helpers": "workspace:*",
"@lucide/shared": "workspace:*",
"@rollup/plugin-replace": "^6.0.3",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@types/semver": "^7.8.0",
"h3": "^1.15.11",
"markdown-it": "^15.0.1",
@@ -41,9 +44,9 @@
"nitropack": "^2.13.4",
"rollup-plugin-copy": "^3.5.0",
"semver": "^7.8.5",
"sharp": "^0.35.4",
"sharp": "^0.35.5",
"shiki": "^4.4.3",
"simple-git": "^3.36.0",
"simple-git": "^4.0.1",
"sitemap": "^9.0.1",
"svg-path-commander": "^2.3.3",
"svg-pathdata": "^9.0.0",
@@ -69,9 +72,9 @@
"lucide": "workspace:*",
"lucide-react": "workspace:*",
"motion-v": "2.4.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.3.0",
"react-dom": "19.3.0",
"sandpack-vue3": "3.1.12",
"vue": "^3.5.42"
"vue": "^3.5.43"
}
}
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32">
<path d="M11.622 24.74s-1.23.748.855.962c2.51.32 3.847.267 6.625-.267a10.02 10.02 0 0 0 1.763.855c-6.25 2.672-14.16-.16-9.244-1.55zm-.8-3.473s-1.336 1.015.748 1.23c2.725.267 4.862.32 8.55-.427a3.26 3.26 0 0 0 1.282.801c-7.534 2.244-15.976.214-10.58-1.603zm14.747 6.09s.908.748-1.015 1.336c-3.58 1.07-15.014 1.39-18.22 0-1.122-.48 1.015-1.175 1.7-1.282.695-.16 1.07-.16 1.07-.16-1.23-.855-8.175 1.763-3.526 2.51 12.77 2.084 23.296-.908 19.983-2.404zM12.2 17.633s-5.824 1.39-2.084 1.87c1.603.214 4.755.16 7.694-.053 2.404-.214 4.81-.64 4.81-.64s-.855.374-1.443.748c-5.93 1.55-17.312.855-14.052-.748 2.778-1.336 5.076-1.175 5.076-1.175zm10.42 5.824c5.984-3.1 3.206-6.09 1.282-5.717-.48.107-.695.214-.695.214s.16-.32.534-.427c3.794-1.336 6.786 4.007-1.23 6.09 0 0 .053-.053.107-.16zm-9.83 8.442c5.77.374 14.587-.214 14.8-2.94 0 0-.427 1.07-4.755 1.87-4.916.908-11.007.8-14.587.214 0 0 .748.64 4.542.855z" fill="#4e7896"/>
<path d="M18.996.001s3.313 3.366-3.152 8.442c-5.183 4.114-1.175 6.465 0 9.137-3.046-2.725-5.236-5.13-3.74-7.373C14.294 6.893 20.332 5.3 18.996.001zm-1.7 15.335c1.55 1.763-.427 3.366-.427 3.366s3.954-2.03 2.137-4.542c-1.656-2.404-2.94-3.58 4.007-7.587 0 0-10.953 2.725-5.717 8.763z" fill="#f58219"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB