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

This commit is contained in:
Eric
2026-10-19 12:02:14 +02:00
46 changed files with 1411 additions and 704 deletions
+12
View File
@@ -0,0 +1,12 @@
[
"fix",
"feat",
"perf",
"refactor",
"test",
"style",
"docs",
"ci",
"build",
"chore"
]
+103
View File
@@ -0,0 +1,103 @@
name: PR title
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- edited
permissions: {}
concurrency:
group: pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
generate-pr-title:
name: Generate PR Title
# Skip when a maintainer has opted out by adding the "Keep PR title" label,
# and on manual edits so they are not reverted straight away.
if: github.repository == 'lucide-icons/lucide' &&
github.event.action != 'edited' &&
!contains(github.event.pull_request.labels.*.name, 'Keep PR title')
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # Required to edit the PR title
steps:
# We check out the main branch of the main repository for security reasons.
# The script only reads its input from the environment, so it never runs
# any code from the (untrusted) pull request.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: lucide-icons/lucide
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: 'package.json'
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
# The checkout is the base branch, so the PR head commit isn't available locally.
use_rest_api: true
- name: Generate PR title
id: generate
run: |
NEW_TITLE="$(node ./scripts/generatePullRequestTitle.mts)"
{
echo "new_title<<PR_TITLE_EOF"
echo "$NEW_TITLE"
echo "PR_TITLE_EOF"
} >> "$GITHUB_OUTPUT"
env:
ADDED_FILES: ${{ steps.changed-files.outputs.added_files }}
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_and_modified_files }}
CURRENT_TITLE: ${{ github.event.pull_request.title }}
- name: Update PR title
if: steps.generate.outputs.new_title != ''
run: gh pr edit "$PR_NUMBER" --title "$NEW_TITLE"
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
NEW_TITLE: ${{ steps.generate.outputs.new_title }}
# Runs after the title is generated: an edit made with `GITHUB_TOKEN` does not
# trigger new workflow runs, so a separate workflow would lint the old title.
# The lint action fetches the current title through the API.
lint-pr-title:
name: PR Title Lint
needs: generate-pr-title
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read # Required to read the PR title
steps:
# Checks out the base branch, so the allowed types can't be changed by the PR.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read conventional commit types
id: commit-types
run: |
{
echo "types<<TYPES_EOF"
jq -r '.[]' .github/conventional-commit-types.json
echo "TYPES_EOF"
} >> "$GITHUB_OUTPUT"
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: ${{ steps.commit-types.outputs.types }}
requireScope: true
ignoreLabels: |
bot
-36
View File
@@ -1,36 +0,0 @@
name: Linting PR title
on:
pull_request:
types:
- opened
- edited
- reopened
permissions:
contents: read
pull-requests: read # Required to read the PR title
jobs:
lint-pr-title:
name: PR Title Lint
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
fix
feat
perf
refactor
test
style
docs
ci
build
chore
requireScope: true
ignoreLabels: |
bot
+1
View File
@@ -1,4 +1,5 @@
pnpm-lock.yaml
.github/conventional-commit-types.json
# docs examples
docs/**/examples/
@@ -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;
@@ -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 }"
@@ -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 -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 -1
View File
@@ -75,6 +75,6 @@
"react": "19.3.0",
"react-dom": "19.3.0",
"sandpack-vue3": "3.1.12",
"vue": "^3.5.43"
"vue": "^3.5.44"
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley",
"devkamani9313",
"karsa-mistmere"
],
"use-cases": [
"Representing fruit, guacamole, or healthy food in nutrition apps",
"Indicating salad, breakfast, or brunch ingredients",
"Showing vegetarian or vegan food options",
"Marking avocado toast as a menu item in restaurant or delivery apps",
"Denoting fresh produce or fruit section in grocery and supermarket apps",
"Signifying healthy fats or superfood in nutrition and wellness trackers",
"Labeling plant-based or vegetarian recipes in cooking websites and apps"
],
"tags": [
"fruit",
"snack",
"meal",
"lunch",
"ingredient",
"cooking",
"cookery",
"kitchen",
"cuisine",
"eating",
"healthy",
"nutrition",
"diet",
"guacamole",
"mexican",
"green",
"pit",
"seed",
"half",
"produce",
"vegetarian",
"vegan",
"grocery",
"supermarket",
"toast",
"superfood",
"plant based",
"ripe"
],
"categories": [
"food-beverage"
]
}
+14
View File
@@ -0,0 +1,14 @@
<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="M13.465 3.464a5 5 0 017.07 0 5 5 0 010 7.072c-1.204 1.203-2.38 2.89-2.614 4.577A8 8 0 118.886 6.08c1.687-.235 3.375-1.41 4.579-2.615" />
<circle cx="10" cy="14" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 391 B

+51
View File
@@ -0,0 +1,51 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley",
"devkamani9313",
"karsa-mistmere"
],
"use-cases": [
"Representing breakfast items, rashers, or pork products",
"Indicating diner, brunch, or fast food menu items",
"Showing savory cooked meats or barbecue items",
"Labeling grocery or deli items in food apps",
"Marking breakfast sandwich or brunch special add-ons",
"Tagging cured or smoked meat ingredients in recipes"
],
"tags": [
"rashers",
"smoked",
"streaky",
"crispy",
"meat",
"fast food",
"junk food",
"snack",
"dish",
"restaurant",
"brunch",
"lunch",
"meal",
"savory",
"savoury",
"cookery",
"cooking",
"grilled",
"barbecue",
"barbeque",
"bbq",
"pig",
"pork",
"breakfast",
"cured",
"butcher",
"sandwich",
"protein",
"grocery",
"kitchen"
],
"categories": [
"food-beverage"
]
}
+14
View File
@@ -0,0 +1,14 @@
<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="M2.795 16.804c-.789-.79-1.09-2.018-.442-2.928 1.933-2.713 4.414-1.05 6.145-5.378 1.868-4.67 4.608-2.366 6.592-6.107.218-.412.78-.53 1.11-.2l5.005 5.005c.789.79 1.09 2.018.442 2.928-1.933 2.713-4.414 1.05-6.145 5.378-1.868 4.67-4.608 2.366-6.591 6.107-.22.412-.78.53-1.11.2z" />
<path d="M4.5 18.5c2-5 5-2 7-7s5-2 7-7" />
</svg>

After

Width:  |  Height:  |  Size: 542 B

+5 -5
View File
@@ -9,9 +9,9 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 3a41 41 0 000 18" />
<path d="M14 3a41 41 0 010 18" />
<path d="M16.997 21a2 2 0 001.68-.92 15.25 15.25 0 000-16.16 2 2 0 00-1.68-.92h-10a2 2 0 00-1.681.92 15.25 15.25 0 000 16.16 2 2 0 001.681.92z" />
<path d="M3.54 16h16.914" />
<path d="M3.54 8h16.914" />
<path d="M10.5 2a34.5 34.5 0 000 20" />
<path d="M13.5 2a34.5 34.5 0 010 20" />
<path d="M4.67 17h14.67" />
<path d="M4.67 7h14.67" />
<path d="M6.23 3.06a19 19 0 000 17.88A2 2 0 008 22h8a2 2 0 001.77-1.06 19 19 0 000-17.88A2 2 0 0016 2H8a2 2 0 00-1.77 1.06" />
</svg>

Before

Width:  |  Height:  |  Size: 490 B

After

Width:  |  Height:  |  Size: 480 B

+44
View File
@@ -0,0 +1,44 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"rrod497"
],
"use-cases": [
"Representing SSL/TLS security certificates",
"Representing educational or professional certifications"
],
"tags": [
"award",
"ribbon",
"medal",
"achievement",
"pinned",
"badge",
"recognition",
"laurel",
"commendation",
"diploma",
"official",
"proof",
"fact",
"verified",
"license",
"warrant",
"authorization",
"legal",
"ownership",
"credential",
"ssl",
"tls",
"security",
"trust",
"cybersecurity",
"authority",
"graduation",
"study"
],
"categories": [
"connectivity",
"security"
]
}
+17
View File
@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 10H6" />
<path d="m20.69 15.479 1.29 4.88a.5.5 0 01-.698.591l-1.843-.849a1 1 0 00-.879.001l-1.846.85a.5.5 0 01-.692-.593l1.29-4.88" />
<path d="M22 6.675V6a2 2 0 00-2-2H4a2 2 0 00-2 2v12a2 2 0 002 2h8" />
<path d="M6 14h4" />
<circle cx="19" cy="13" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 490 B

+5 -5
View File
@@ -9,11 +9,11 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 20a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z" />
<path d="m12.474 5.943 1.567 5.34a1 1 0 0 0 1.75.328l2.616-3.402" />
<path d="m20 9-3 9" />
<path d="m5.594 8.209 2.615 3.403a1 1 0 0 0 1.75-.329l1.567-5.34" />
<path d="M7 18 4 9" />
<path d="m10.68 5.499-.996 4.982a.65.65 0 01-1.131.295L5.885 7.663" />
<path d="m13.32 5.5.995 4.98a.65.65 0 001.131.296l2.668-3.113" />
<path d="M20.428 8.952 17.5 18" />
<path d="M4 20a2 2 0 012-2h12a2 2 0 012 2v1a1 1 0 01-1 1H5a1 1 0 01-1-1z" />
<path d="M6.5 18 3.571 8.952" />
<circle cx="12" cy="4" r="2" />
<circle cx="20" cy="7" r="2" />
<circle cx="4" cy="7" r="2" />

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 601 B

+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"it-is-not",
"danielbayley",
"karsa-mistmere",
"jamiemlaw"
],
"use-cases": [
"Representing music or an audio track",
"Representing a single eighth note in music notation"
],
"tags": [
"audio",
"music",
"sound",
"note",
"quaver"
],
"categories": [
"multimedia",
"files"
],
"aliases": [
{
"name": "music-2",
"deprecationReason": "alias.name",
"deprecated": true
}
]
}
+2 -2
View File
@@ -9,6 +9,6 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="8" cy="18" r="4" />
<path d="M12 18V2l7 4" />
<path d="m19 6-5.2-3.9a.5.5 0 0 0-.8.4V18" />
<circle cx="9" cy="18" r="4" />
</svg>

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 290 B

+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"colebemis",
"csandman",
"mittalyashu",
"ericfennis",
"jamiemlaw"
],
"use-cases": [
"Representing music or an audio track",
"Representing paired eighth notes in music notation"
],
"tags": [
"audio",
"music",
"sound",
"notes",
"quavers"
],
"categories": [
"multimedia",
"files"
],
"aliases": [
{
"name": "music",
"deprecationReason": "alias.name",
"deprecated": true
}
]
}
+2 -3
View File
@@ -9,8 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 18V5l12-2v13" />
<path d="m9 9 12-2" />
<circle cx="6" cy="18" r="3" />
<path d="M21 16V3a1 1 0 0 0-1.28-.96l-10 2.917a1 1 0 0 0-.72.96V19" />
<circle cx="18" cy="16" r="3" />
<circle cx="6" cy="19" r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 350 B

+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"it-is-not",
"danielbayley",
"karsa-mistmere"
],
"use-cases": [
"Representing music or an audio track",
"Representing a half note in music notation"
],
"tags": [
"audio",
"music",
"sound",
"note",
"crotchet",
"minim",
"quarter note"
],
"categories": [
"multimedia",
"files"
],
"aliases": [
{
"name": "music-3",
"deprecationReason": "alias.name",
"deprecated": true
}
]
}

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"madebyoz"
],
"use-cases": [
"Adjusting spacing between characters in text editors and design tools"
],
"tags": [
"tracking",
"kerning",
"design",
"lettering",
"text",
"letters",
"characters",
"font",
"typography",
"formatting"
],
"categories": [
"text"
]
}
+4 -2
View File
@@ -9,6 +9,8 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 8c-2 5-5 2-7 7s-5 2-7 7l-6-6c2-5 5-2 7-7s5-2 7-7Z" />
<path d="M5 19c2-5 5-2 7-7s5-2 7-7" />
<path d="M15 14H9" />
<path d="M21 3v18" />
<path d="M3 3v18" />
<path d="m8 16 3.453-7.648a.6.6 0 011.094.001L16 16" />
</svg>

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 337 B

-18
View File
@@ -1,18 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"it-is-not",
"danielbayley",
"karsa-mistmere"
],
"use-cases": [],
"tags": [
"quaver",
"eighth note",
"note"
],
"categories": [
"multimedia",
"files"
]
}
-20
View File
@@ -1,20 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"it-is-not",
"danielbayley",
"karsa-mistmere"
],
"use-cases": [],
"tags": [
"crotchet",
"minim",
"quarter note",
"half note",
"note"
],
"categories": [
"multimedia",
"files"
]
}
-17
View File
@@ -1,17 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"it-is-not",
"karsa-mistmere"
],
"use-cases": [],
"tags": [
"semiquaver",
"sixteenth note",
"note"
],
"categories": [
"multimedia",
"files"
]
}
-19
View File
@@ -1,19 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"colebemis",
"csandman",
"mittalyashu",
"ericfennis"
],
"use-cases": [],
"tags": [
"note",
"quaver",
"eighth note"
],
"categories": [
"multimedia",
"files"
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"it-is-not",
"karsa-mistmere",
"jamiemlaw"
],
"use-cases": [
"Representing music or an audio track",
"Representing paired sixteenth notes in music notation"
],
"tags": [
"audio",
"music",
"sound",
"notes",
"semiquavers"
],
"categories": [
"multimedia",
"files"
],
"aliases": [
{
"name": "music-4",
"deprecationReason": "alias.name",
"deprecated": true
}
]
}
+16
View File
@@ -0,0 +1,16 @@
<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="M21 16V3a1 1 0 0 0-1.28-.96l-10 2.917a1 1 0 0 0-.72.96V19" />
<path d="M21 6 9 9.5" />
<circle cx="18" cy="16" r="3" />
<circle cx="6" cy="19" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

+40
View File
@@ -0,0 +1,40 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"hitanshu-machhi",
"colebemis",
"csandman",
"ericfennis",
"karsa-mistmere",
"jguddas",
"mittalyashu"
],
"use-cases": [
"Representing a user profile in a mobile application",
"Indicating a mobile account or device owner",
"Showing mobile contact or identity settings",
"Displaying a user avatar in a mobile dashboard",
"Linking to a contact's profile from a mobile list",
"Indicating mobile sign-in or authentication screens",
"Marking a device as assigned to a user in device management apps"
],
"tags": [
"phone",
"cellphone",
"device",
"mobile",
"person",
"profile",
"account",
"avatar",
"contact",
"identity",
"owner",
"authentication"
],
"categories": [
"account",
"devices",
"people"
]
}
@@ -9,6 +9,8 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 7a4.95 4.95 0 0 0-8.6-3.4c-1.5 1.6-1.6 1.8-5 2.6a8 8 0 1 0 9.4 9.5c.7-3.4 1-3.6 2.6-5 1-1 1.6-2.3 1.6-3.7" />
<circle cx="10" cy="14" r="3.5" />
<path d="M10 22H6a2 2 0 01-2-2V4a2 2 0 012-2h10a2 2 0 012 2v4" />
<path d="M11 18h.01" />
<path d="M22 22a4 4 0 00-8 0" />
<circle cx="18" cy="15" r="3" />
</svg>

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 372 B

+41
View File
@@ -0,0 +1,41 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"jguddas",
"ryck"
],
"use-cases": [
"Marking death or game-over states in games, often paired with skull or ghost icons",
"Indicating cemetery or graveyard locations on maps",
"Marking deceased individuals in genealogy or family-tree applications",
"Flagging obsolete, deprecated, or retired items such as endpoints or pages"
],
"tags": [
"grave",
"gravestone",
"headstone",
"cemetery",
"graveyard",
"death",
"memorial",
"funeral",
"halloween",
"rip",
"obituary",
"deceased",
"epitaph",
"mourning",
"bereavement",
"condolence",
"deprecated",
"retired",
"archived",
"decommissioned"
],
"categories": [
"gaming",
"navigation",
"people",
"development"
]
}
+4 -3
View File
@@ -9,7 +9,8 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 18V5l12-2v13" />
<circle cx="6" cy="18" r="3" />
<circle cx="18" cy="16" r="3" />
<path d="M11 10h2" />
<path d="M19 21V10a7 7 0 00-14 0v11" />
<path d="M2 21h20" />
<path d="M9 14h6" />
</svg>

Before

Width:  |  Height:  |  Size: 308 B

After

Width:  |  Height:  |  Size: 321 B

+2 -2
View File
@@ -12,11 +12,11 @@
"dependencies": {
"@lucide/vue": "workspace:*",
"nuxt": "4.5.1",
"vue": "3.5.43",
"vue": "3.5.44",
"vue-router": "5.3.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue": "^6.0.10",
"@vitest/browser-playwright": "5.0.3",
"typescript": "^6.0.3",
"vite": "^8.3.2",
+2 -2
View File
@@ -13,10 +13,10 @@
"@lucide/vue": "workspace:*",
"vike": "0.4.266",
"vike-vue": "0.9.13",
"vue": "3.5.43"
"vue": "3.5.44"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue": "^6.0.10",
"@vitest/browser-playwright": "5.0.3",
"typescript": "^6.0.3",
"vite": "^8.3.2",
+2 -2
View File
@@ -11,12 +11,12 @@
},
"dependencies": {
"@lucide/vue": "workspace:*",
"vue": "3.5.43"
"vue": "3.5.44"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.5",
"@types/node": "^26.1.1",
"@vitejs/plugin-vue": "^6.0.8",
"@vitejs/plugin-vue": "^6.0.10",
"@vitest/browser-playwright": "5.0.3",
"@vue/tsconfig": "^0.9.1",
"playwright": "1.58.2",
-26
View File
@@ -1,26 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley"
],
"tags": [
"fruit",
"snack",
"meal",
"lunch",
"ingredient",
"cooking",
"cookery",
"kitchen",
"cuisine",
"eating",
"healthy",
"nutrition",
"diet",
"guacamole",
"mexican"
],
"categories": [
"food-beverage"
]
}
-33
View File
@@ -1,33 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"danielbayley"
],
"tags": [
"rashers",
"smoked",
"streaky",
"crispy",
"meat",
"fast food",
"junk food",
"snack",
"dish",
"restaurant",
"brunch",
"lunch",
"meal",
"savory",
"savoury",
"cookery",
"cooking",
"grilled",
"barbecue",
"barbeque",
"bbq",
"pig"
],
"categories": [
"food-beverage"
]
}
+1
View File
@@ -11,6 +11,7 @@
"test:integrations:vue": "pnpm --filter @lucide/vue build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-vue/**' check",
"test:integrations:svelte": "pnpm --filter @lucide/svelte build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-svelte/**' check",
"test:font": "pnpm --filter build-font test",
"test:scripts": "node --test scripts/*.test.mts",
"lucide": "pnpm --filter lucide",
"lucide-angular": "pnpm --filter lucide-angular",
"lucide-react": "pnpm --filter lucide-react",
+1 -1
View File
@@ -77,7 +77,7 @@
"jest-serializer-html": "^7.1.0",
"react": "19.3.0",
"react-dom": "19.3.0",
"react-native": "^0.87.1",
"react-native": "^0.88.0",
"react-native-svg": "^15.15.5",
"rollup": "^4.63.1",
"rollup-plugin-dts": "^6.5.1",
+2 -2
View File
@@ -46,13 +46,13 @@
"@lucide/shared": "workspace:*",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/vue": "^8.1.0",
"@vitejs/plugin-vue": "^6.0.9",
"@vitejs/plugin-vue": "^6.0.10",
"@vue/test-utils": "2.5.1",
"rollup": "^4.63.1",
"rollup-plugin-dts": "^6.5.1",
"typescript": "^6.0.3",
"vite": "^8.3.2",
"vue": "^3.5.43"
"vue": "^3.5.44"
},
"peerDependencies": {
"vue": ">=3.0.1"
+406 -458
View File
File diff suppressed because it is too large Load Diff
+245
View File
@@ -0,0 +1,245 @@
/**
* Generates a conventional-commit PR title from the paths a pull request changes.
*
* Because merges in this repository are squashed, the PR title becomes the
* commit message and the changelog entry. This script generates the whole title
* for icon contributions and, for every other kind of PR, inserts or updates the
* conventional-commit scope so `lint-pr-title` is satisfied.
*
* It follows the convention of `generateChangedIconsCommentMarkup.mts`: it reads
* its input from the environment, prints the result to stdout and has no other
* side effects. It only imports Node built-ins and the shared
* `conventional-commit-types.json` (a native JSON import), so Node can still run
* it natively as an `.mts` file without a `pnpm install` step.
*
* Env input (CLI wrapper at the bottom):
* - `ADDED_FILES_FILE`: path to a newline-separated list of added files, or
* - `ADDED_FILES`: a space-separated list of added files (handy for local runs).
* - `CHANGED_FILES_FILE` / `CHANGED_FILES`: the same for all changed files
* (added, modified, renamed and deleted).
* - `CURRENT_TITLE`: the current PR title.
*
* Output: the new title on stdout, or nothing at all when no change is needed.
*/
import { readFileSync } from 'node:fs';
// Single source of truth for the conventional-commit types, shared with
// `lint-pr-title` (amannn/action-semantic-pull-request).
import VALID_TYPES from '../.github/conventional-commit-types.json' with { type: 'json' };
// Default type used in prefix mode when the current title has no valid type.
// The path reliably tells us the scope but nothing about whether a change is a
// feat/fix/chore, so we fall back to the most neutral type.
const DEFAULT_TYPE = 'chore';
// How many icon names to list in a generated title before summarising the rest.
const MAX_NAMES = 3;
// How many package scopes to join with `|` before falling back to `packages`.
const MAX_SCOPES = 3;
const PACKAGES_SCOPE = 'packages';
/**
* - `icon`: an added file *is* the subject, so the whole title is generated.
* Changed (not added) icon files only provide a scope.
* - `package`: a package in `packages/`, scoped by its `package.json` name.
* - `repo`: any other area of the repository.
*/
type Kind = 'icon' | 'package' | 'repo';
type Rule = {
pattern: RegExp;
scope: string;
kind: Kind;
};
/**
* Path -> scope rules. Each file is matched by the *first* rule in this list
* that matches it, and scopes are listed in the order of this list, which keeps
* the outcome deterministic. The `icons`, `lab`, `metadata`, `docs` and `site`
* scopes mirror `.github/labeler.yml`; `packages/*` scopes mirror each
* package's `package.json` name.
*/
const RULES: Rule[] = [
{ pattern: /^icons\/[^/]+\.svg$/, scope: 'icons', kind: 'icon' },
{ pattern: /^lab\/[^/]+\.svg$/, scope: 'lab', kind: 'icon' },
{ pattern: /^packages\/lucide-react-native\//, scope: 'lucide-react-native', kind: 'package' },
{ pattern: /^packages\/lucide-react\//, scope: 'lucide-react', kind: 'package' },
{ pattern: /^packages\/lucide-preact\//, scope: 'lucide-preact', kind: 'package' },
{ pattern: /^packages\/lucide-solid\//, scope: 'lucide-solid', kind: 'package' },
{ pattern: /^packages\/lucide-static\//, scope: 'lucide-static', kind: 'package' },
{ pattern: /^packages\/icons\//, scope: '@lucide/icons', kind: 'package' },
{ pattern: /^packages\/vue\//, scope: '@lucide/vue', kind: 'package' },
{ pattern: /^packages\/angular\//, scope: '@lucide/angular', kind: 'package' },
{ pattern: /^packages\/svelte\//, scope: '@lucide/svelte', kind: 'package' },
{ pattern: /^packages\/solid\//, scope: '@lucide/solid', kind: 'package' },
{ pattern: /^packages\/astro\//, scope: '@lucide/astro', kind: 'package' },
{ pattern: /^packages\/lab\//, scope: '@lucide/lab', kind: 'package' },
{ pattern: /^packages\/shared\//, scope: '@lucide/shared', kind: 'package' },
{ pattern: /^packages\/lucide\//, scope: 'lucide', kind: 'package' },
{ pattern: /^(?:icons|categories)\/[^/]+\.json$/, scope: 'metadata', kind: 'repo' },
{ pattern: /^docs\/.*\.md$/, scope: 'docs', kind: 'repo' },
{ pattern: /^docs\//, scope: 'site', kind: 'repo' },
];
/**
* Every scope this script can produce. A title whose scope consists only of
* these is owned by the script and may be updated; any other scope was chosen
* by a person and is kept.
*/
const GENERATED_SCOPES = new Set([...RULES.map((rule) => rule.scope), PACKAGES_SCOPE]);
/**
* Formats a list of icon names into the subject of a generated title, e.g.
* `` `heart` icon ``, `` `heart`, `heart-off` icons `` or
* `` `a`, `b`, `c` and 2 more icons ``.
*/
const formatIconNames = (names: string[]): string => {
const quoted = names.map((name) => `\`${name}\``);
const noun = names.length === 1 ? 'icon' : 'icons';
if (quoted.length <= MAX_NAMES) {
return `${quoted.join(', ')} ${noun}`;
}
const shown = quoted.slice(0, MAX_NAMES).join(', ');
const remaining = quoted.length - MAX_NAMES;
return `${shown} and ${remaining} more ${noun}`;
};
/**
* Builds the whole title for an icon rule from the added files matching it.
* Names are deduped by basename (an icon PR adds both `heart.svg` and
* `heart.json`) and kept in the order they first appear.
*/
const generateTitle = (scope: string, matchedFiles: string[]): string => {
const names = matchedFiles
.map((file) => file.replace(/^.*\//, '').replace(/\.[^.]+$/, ''))
.filter((name, index, all) => all.indexOf(name) === index);
return `feat(${scope}): added ${formatIconNames(names)}`;
};
/**
* Derives the scope from the changed files. Package scopes win over icon
* scopes, which win over any other scope (an icon change usually touches its
* metadata too). Multiple scopes are joined with `|`, e.g.
* `lucide-react|@lucide/vue`, and more than `MAX_SCOPES` packages are
* summarised as `packages`.
*/
const resolveScope = (changedFiles: string[]): string | null => {
const matchedRules = new Set(
changedFiles
.map((file) => RULES.find((rule) => rule.pattern.test(file)))
.filter((rule) => rule != null),
);
const scopesOf = (kind: Kind) =>
RULES.filter((rule) => rule.kind === kind && matchedRules.has(rule)).map((rule) => rule.scope);
const packageScopes = scopesOf('package');
if (packageScopes.length > MAX_SCOPES) return PACKAGES_SCOPE;
if (packageScopes.length > 0) return packageScopes.join('|');
for (const kind of ['icon', 'repo'] as const) {
const scopes = scopesOf(kind);
if (scopes.length > 0) return scopes.join('|');
}
return null;
};
const isGeneratedScope = (scope: string): boolean =>
scope.split('|').every((part) => GENERATED_SCOPES.has(part.trim()));
/**
* Rewrites the current title around the resolved scope: the contributor's own
* type and subject are preserved and the title is normalised to
* `type(scope)!: subject`. A scope chosen by a person is kept; a missing scope or
* one this script generated earlier is replaced by `scope`. When the title has
* no valid type, `DEFAULT_TYPE` is used. When there is no subject or no scope to
* work with, `null` is returned so the title is left untouched.
*/
const prefixTitle = (scope: string | null, currentTitle: string): string | null => {
const match = currentTitle.match(/^\s*(\w+)\s*(?:\(([^)]*)\))?\s*(!?)\s*:\s*(.*)$/);
const type = match?.[1].toLowerCase();
if (match && type != null && VALID_TYPES.includes(type)) {
const [, , currentScope = '', breaking, subject] = match;
if (subject.trim() === '') return null;
const keepScope =
currentScope.trim() !== '' && (scope == null || !isGeneratedScope(currentScope));
const newScope = keepScope ? currentScope.trim() : scope;
if (newScope == null) return null;
return `${type}(${newScope})${breaking}: ${subject.trim()}`;
}
const subject = currentTitle.trim();
if (scope == null || subject === '') return null;
return `${DEFAULT_TYPE}(${scope}): ${subject}`;
};
type PullRequestFiles = {
addedFiles: string[];
changedFiles: string[];
};
/**
* Pure title function: given the added and changed files and the current
* title, returns the new title, or `null` when nothing should change (no
* matching rule, no subject to work with, or the title is already correct).
*/
export const generatePullRequestTitle = (
{ addedFiles, changedFiles }: PullRequestFiles,
currentTitle: string,
): string | null => {
const iconRule = RULES.find(
(rule) => rule.kind === 'icon' && addedFiles.some((file) => rule.pattern.test(file)),
);
// Added files are changed files too, also when only `ADDED_FILES` is given.
const allChangedFiles = [...new Set([...addedFiles, ...changedFiles])];
const newTitle =
iconRule != null
? generateTitle(
iconRule.scope,
addedFiles.filter((file) => iconRule.pattern.test(file)),
)
: prefixTitle(resolveScope(allChangedFiles), currentTitle);
if (newTitle == null || newTitle === currentTitle) return null;
return newTitle;
};
// --- CLI wrapper -----------------------------------------------------------
// Only runs when executed directly, keeping the function above unit-testable.
const readFileList = (filePath: string | undefined, list: string | undefined): string[] => {
const raw = filePath != null && filePath !== '' ? readFileSync(filePath, 'utf-8') : (list ?? '');
return raw
.split(/\s+/)
.map((file) => file.trim())
.filter((file) => file !== '');
};
if (import.meta.main) {
const { ADDED_FILES_FILE, ADDED_FILES, CHANGED_FILES_FILE, CHANGED_FILES, CURRENT_TITLE } =
process.env;
const newTitle = generatePullRequestTitle(
{
addedFiles: readFileList(ADDED_FILES_FILE, ADDED_FILES),
changedFiles: readFileList(CHANGED_FILES_FILE, CHANGED_FILES),
},
CURRENT_TITLE ?? '',
);
if (newTitle != null) {
process.stdout.write(newTitle);
}
}
+160
View File
@@ -0,0 +1,160 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { generatePullRequestTitle } from './generatePullRequestTitle.mts';
const added = (addedFiles: string[], title: string) =>
generatePullRequestTitle({ addedFiles, changedFiles: [] }, title);
const changed = (changedFiles: string[], title: string) =>
generatePullRequestTitle({ addedFiles: [], changedFiles }, title);
describe('generatePullRequestTitle', () => {
describe('icon titles', () => {
it('generates a title for a single icon and dedupes svg and json', () => {
assert.equal(
added(['icons/heart.svg', 'icons/heart.json'], 'add heart'),
'feat(icons): added `heart` icon',
);
});
it('lists multiple icons', () => {
assert.equal(
added(['icons/heart.svg', 'icons/heart-off.svg'], 'icons'),
'feat(icons): added `heart`, `heart-off` icons',
);
});
it('summarises many icons', () => {
assert.equal(
added(['icons/a.svg', 'icons/b.svg', 'icons/c.svg', 'icons/d.svg', 'icons/e.svg'], 'x'),
'feat(icons): added `a`, `b`, `c` and 2 more icons',
);
});
it('generates a title for lab icons', () => {
assert.equal(added(['lab/ghost.svg'], 'x'), 'feat(lab): added `ghost` icon');
});
it('returns null when the title is already correct', () => {
assert.equal(added(['icons/heart.svg'], 'feat(icons): added `heart` icon'), null);
});
it('ignores other changed files when an icon is added', () => {
assert.equal(
generatePullRequestTitle(
{ addedFiles: ['icons/heart.svg'], changedFiles: ['docs/guide.md'] },
'x',
),
'feat(icons): added `heart` icon',
);
});
});
describe('scope resolution', () => {
it('uses the package name as scope', () => {
assert.equal(
changed(['packages/lucide-react/src/a.ts'], 'fix: crash'),
'fix(lucide-react): crash',
);
});
it('joins multiple packages with a pipe', () => {
assert.equal(
changed(['packages/vue/src/b.ts', 'packages/lucide-react/src/a.ts'], 'chore: Fix export'),
'chore(lucide-react|@lucide/vue): Fix export',
);
});
it('summarises more than three packages as `packages`', () => {
assert.equal(
changed(
[
'packages/lucide-react/a.ts',
'packages/vue/a.ts',
'packages/svelte/a.ts',
'packages/angular/a.ts',
],
'refactor: shared types',
),
'refactor(packages): shared types',
);
});
it('prefers packages over docs', () => {
assert.equal(
changed(['docs/guide/react.md', 'packages/lucide-react/src/a.ts'], 'feat: new prop'),
'feat(lucide-react): new prop',
);
});
it('joins docs and site scopes', () => {
assert.equal(
changed(['docs/guide/a.md', 'docs/.vitepress/theme/b.vue'], 'docs: update'),
'docs(docs|site): update',
);
});
it('uses `metadata` for icon and category json files', () => {
assert.equal(changed(['icons/heart.json'], 'fix: tags'), 'fix(metadata): tags');
assert.equal(changed(['categories/shapes.json'], 'fix: category'), 'fix(metadata): category');
});
it('prefers icons over metadata for changed icons', () => {
assert.equal(
changed(['icons/heart.svg', 'icons/heart.json'], 'fix: heart'),
'fix(icons): heart',
);
});
it('supports @lucide/solid', () => {
assert.equal(changed(['packages/solid/src/a.ts'], 'feat: x'), 'feat(@lucide/solid): x');
});
it('does not match lucide-react for lucide-react-native files', () => {
assert.equal(
changed(['packages/lucide-react-native/src/a.ts'], 'fix: x'),
'fix(lucide-react-native): x',
);
});
it('returns null when no rule matches', () => {
assert.equal(changed(['README.md'], 'chore: readme'), null);
});
});
describe('prefix titles', () => {
const files = ['packages/lucide-react/src/a.ts'];
it('keeps a custom scope', () => {
assert.equal(changed(files, 'fix(react): crash'), null);
});
it('extends a generated scope', () => {
assert.equal(
changed([...files, 'packages/vue/src/a.ts'], 'fix(lucide-react): crash'),
'fix(lucide-react|@lucide/vue): crash',
);
});
it('normalises the type casing', () => {
assert.equal(changed(files, 'Fix: crash'), 'fix(lucide-react): crash');
});
it('normalises whitespace and keeps a custom scope', () => {
assert.equal(changed(files, 'feat (react): x'), 'feat(react): x');
});
it('keeps the breaking-change marker', () => {
assert.equal(changed(files, 'feat!: drop old api'), 'feat(lucide-react)!: drop old api');
});
it('returns null for an empty subject', () => {
assert.equal(changed(files, 'fix:'), null);
});
it('falls back to the default type', () => {
assert.equal(changed(files, 'Fix crash'), 'chore(lucide-react): Fix crash');
});
});
});