Merge branch 'main' of https://github.com/lucide-icons/lucide into better-icon-detail-page

This commit is contained in:
Eric Fennis
2026-08-19 11:27:23 +02:00
1323 changed files with 27332 additions and 12629 deletions

View File

@@ -9,3 +9,4 @@ strikethrough
touchpad
ungroup
toc
sunlounger

View File

@@ -1,11 +0,0 @@
dist
build
coverage
lib
tests
node_modules
.eslintrc.js
docs/images
docs/**/examples/
packages/lucide-react/dynamicIconImports.js
packages/angular/.angular

View File

@@ -1,75 +0,0 @@
const DEFAULT_ATTRS = require('./tools/build-icons/render/default-attrs.json');
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: ['airbnb-base', 'prettier'],
plugins: ['import', '@html-eslint'],
rules: {
'no-console': 'off',
'no-param-reassign': 'off',
'no-shadow': 'off',
'no-use-before-define': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['**/*.test.js', '**/*.spec.js', '**/scripts/**'],
},
],
'import/extensions': [
'error',
{
pattern: {
mjs: 'always',
json: 'always',
},
},
],
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./docs/tsconfig.json', './packages/*/tsconfig.json'],
ecmaVersion: 'latest',
sourceType: 'module',
},
overrides: [
{
files: ['./icons/*.svg'],
parser: '@html-eslint/parser',
rules: {
'@html-eslint/require-doctype': 'off',
'@html-eslint/no-duplicate-attrs': 'error',
'@html-eslint/no-inline-styles': 'error',
'@html-eslint/require-attrs': [
'error',
...Object.entries(DEFAULT_ATTRS).map(([attr, value]) => ({
tag: 'svg',
attr,
value: String(value),
})),
],
'@html-eslint/indent': ['error', 2],
'@html-eslint/no-multiple-empty-lines': ['error', { max: 0 }],
'@html-eslint/no-extra-spacing-attrs': [
'error',
{
enforceBeforeSelfClose: true,
},
],
'@html-eslint/require-closing-tags': [
'error',
{
selfClosing: 'always',
allowSelfClosingCustom: true,
},
],
'@html-eslint/element-newline': 'error',
'@html-eslint/no-trailing-spaces': 'error',
'@html-eslint/quotes': 'error',
},
},
],
};

View File

@@ -0,0 +1,50 @@
name: Icon request
description: Suggest a new icon for this project
body:
- type: markdown
attributes:
value: |
Before submitting an icon request check if it has already been requested. If there is an open request, please add a 👍.
> [!CAUTION]
> New brand logos are **not** allowed, see our official statement: https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md.
>
> Existing brand icons have been phased out. Consider using https://simpleicons.org, which offers purpose-built SVGs that are also on a 24×24px grid.
- type: input
id: name
attributes:
label: Icon name
description: What should this icon depict? For multiple icons, provide a comma-separated list.
validations:
required: true
- type: textarea
id: use-cases
attributes:
label: Use cases
description: Why do you need this icon? Include at least two real-life use cases per requested icon, avoiding generic descriptions like "it's a car icon".
placeholder: e.g. I need a star icon to use in my rating system.
validations:
required: true
- type: textarea
id: design-ideas
attributes:
label: Design ideas
description: What should this icon look like? Provide simple, minimalistic icon examples from other sets or your own drafts to help us visualize your request.
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Please review the following checklist before submitting your request.
options:
- label: I have searched if someone has submitted a similar request before and there weren't any.
required: true
- label: I have searched existing icons to make sure it does not already exist and I didn't find any.
required: true
- label: I am not requesting a brand logo and the art is not protected by copyright, see more at https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md
required: true
- label: I am not requesting an icon that includes religious, war/violence related, political imagery or hate symbols.
required: true
- label: I have provided appropriate use cases for the icon(s) requested.
required: true

106
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
groups:
react-deps:
patterns:
- '@testing-library/react'
- '@types/prop-types'
- '@types/react'
- '@vitejs/plugin-react'
- 'react'
- 'react-dom'
vue-deps:
patterns:
- '@testing-library/vue'
- '@vitejs/plugin-vue'
- '@vue/test-utils'
- 'vue'
svelte-deps:
patterns:
- '@sveltejs/*'
- '@testing-library/svelte'
- '@tsconfig/svelte'
- 'svelte'
- 'svelte-check'
- 'svelte-preprocess'
solid-deps:
patterns:
- '@babel/*'
- '@rollup/plugin-babel'
- '@solidjs/testing-library'
- 'babel-preset-solid'
- 'solid-js'
- 'vite-plugin-solid'
react-native-deps:
patterns:
- 'react-native'
- 'react-native-svg'
angular-deps:
patterns:
- '@angular/*'
- '@angular-eslint/*'
- '@vitest/browser-playwright'
- 'angular-eslint'
- 'ng-packagr'
- 'rxjs'
- 'tslib'
preact-deps:
patterns:
- '@preact/preset-vite'
- '@testing-library/preact'
- 'preact'
astro-deps:
patterns:
- '@astrojs/*'
- 'astro'
testing-deps:
patterns:
- '@testing-library/*'
- '@vitest/*'
- 'jest-serializer-html'
- 'jsdom'
- 'linkedom'
- 'vitest'
build-deps:
patterns:
- '@rollup/*'
- '@vitejs/*'
- 'esbuild'
- 'rollup'
- 'rollup-plugin-*'
- 'typescript'
- 'vite'
- 'vite-plugin-*'
linting-deps:
patterns:
- '@eslint/*'
- '@html-eslint/*'
- '@typescript-eslint/*'
- 'angular-eslint'
- 'cspell'
- 'eslint'
- 'eslint-*'
- 'husky'
- 'lint-staged'
- 'prettier'
- 'prettier-plugin-*'
types-deps:
patterns:
- '@types/*'
remaining-deps:
patterns:
- '*'
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
groups:
github-actions:
patterns:
- '*'

View File

@@ -13,7 +13,7 @@ The `contributors` property is a required array of GitHub usernames for the peop
The `tags` property is an array of strings that describe the icon and can be used for searching.
Validate the tags against the `icon.schema.json` to ensure they are correctly formatted and adhere to the defined structure.
Provide tag suggestions based on the name of the icon and the use cases provided in the PR description. Use the existing tags in the repository as a reference for consistency and to avoid duplicates. Don't suggest words like: 'icon' and preferably use single words. Tags should always be in lowercase and should not contain spaces. The name of icon should not be included in the tags, as it is already specified.
Provide tag suggestions based on the name of the icon and the use cases provided in the PR description. Use the existing tags in the repository as a reference for consistency and to avoid duplicates. Don't suggest words like: 'icon' and preferably use single words. Tags should always be in lowercase, and may also contain spaces (e.g. `magnifying glass`). The name of icon should not be included in the tags, as it is already specified.
# Categories

View File

@@ -47,7 +47,9 @@ Common scopes: icons, docs, studio, site, dev
### Design <!-- ONLY for new icons -->
<!-- All of these requirements must be fulfilled. -->
- [ ] I've read and followed the [icon design guidelines](https://lucide.dev/contribute/icon-design-guide)
- [ ] I've made sure that the icons don't [already exist](https://lucide.dev/icons), including the [Lab directory](https://github.com/lucide-icons/lucide/tree/main/lab).
- [ ] I've made sure that the icons look sharp on low DPI displays.
- [ ] I've made sure that the icons look consistent with the icon set in size, optical volume and density.
- [ ] I've made sure that the icons are visually centered.

View File

@@ -7,13 +7,16 @@ on:
- tools/build-icons/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -27,9 +30,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -23,13 +23,7 @@ on:
- beta
- alpha
permissions:
id-token: write # Required for OIDC
contents: write
# This is added to make sure we wait until last release is done, this is needed for the font-build process
concurrency:
group: continuous-integration
permissions: {}
jobs:
check-dispatch-gate:
@@ -37,10 +31,12 @@ jobs:
startsWith(github.event.head_commit.message, 'feat(icons)') &&
github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
RELEASE_ENVIRONMENT: ${{ steps.environment.outputs.RELEASE_ENVIRONMENT }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Fetch tags
run: git fetch --all --tags
@@ -64,12 +60,14 @@ jobs:
needs: check-dispatch-gate
environment: ${{ needs.check-dispatch-gate.outputs.RELEASE_ENVIRONMENT }}
runs-on: ubuntu-latest
permissions:
contents: write # Required to create the release and tag
outputs:
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -104,7 +102,7 @@ jobs:
echo '${{ steps.change-log.outputs.CHANGE_LOG }}'
- name: Create Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ steps.new-version.outputs.NEW_VERSION }}
name: Version ${{ steps.new-version.outputs.NEW_VERSION }}
@@ -115,13 +113,19 @@ jobs:
needs: create-release
uses: './.github/workflows/release.yml'
secrets: inherit
permissions:
id-token: write # Required for OIDC (npm provenance)
contents: write # Required to upload release assets
with:
version: ${{ needs.create-release.outputs.VERSION }}
dispatch-release:
if: github.event_name == 'workflow_dispatch'
if: github.repository == 'lucide-icons/lucide' && github.event_name == 'workflow_dispatch'
uses: './.github/workflows/release.yml'
secrets: inherit
permissions:
id-token: write # Required for OIDC (npm provenance)
contents: write # Required to upload release assets
with:
version: ${{ inputs.version }}
tag: ${{ inputs.tag }}

View File

@@ -4,14 +4,21 @@ on:
issues:
types: [opened]
permissions: {}
env:
GH_TOKEN: ${{ github.token }}
jobs:
block_phrases:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Load stopwords from JSON & check issue title & body
if: contains(github.event.issue.labels.*.name, '🙌 icon request')
@@ -33,7 +40,7 @@ jobs:
Lucide **does not accept** brand logos, and we do not plan to add them in the future. This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
[Click here to read our official statement about brand logos in Lucide.](./BRAND_LOGOS_STATEMENT.md)
[Click here to read our official statement about brand logos in Lucide.](https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md)
You can [search for similar issues.](https://github.com/lucide-icons/lucide/issues?q=is%3Aissue+${VALUE})
@@ -43,6 +50,3 @@ jobs:
exit 0
fi
done
env:
GH_TOKEN: ${{ github.token }}

View File

@@ -3,13 +3,16 @@ on:
schedule:
- cron: '45 1 * * *'
permissions: {}
jobs:
stale:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/stale@v9
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-pr-message: This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 5 days.

View File

@@ -6,15 +6,22 @@ on:
types:
- completed
permissions: {}
jobs:
upload:
runs-on: ubuntu-latest
permissions:
contents: read
actions: read # Required to download the artifact from the triggering run
pull-requests: write
if: >
github.repository == 'lucide-icons/lucide' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Download artifact'
uses: actions/github-script@v7
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -42,7 +49,7 @@ jobs:
id: pr-number
- name: Find Comment
uses: peter-evans/find-comment@v2
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: pr-comment
with:
issue-number: ${{ steps.pr-number.outputs.number }}
@@ -50,7 +57,7 @@ jobs:
body-includes: Added or changed icons
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v3
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
comment-id: ${{ steps.pr-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.number }}

View File

@@ -9,13 +9,16 @@ on:
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -29,9 +32,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -2,12 +2,15 @@ name: 'Pull Request Labeler'
on:
- pull_request_target
permissions: {}
jobs:
triage:
if: github.repository == 'lucide-icons/lucide'
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/labeler@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0

View File

@@ -1,17 +1,18 @@
name: Linting PR
name: Linting PR code
on:
pull_request:
paths-ignore:
- icons/*.svg
permissions:
contents: read
jobs:
lint-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -1,4 +1,4 @@
name: Linting PR
name: Linting PR title
on:
pull_request:
@@ -7,12 +7,16 @@ on:
- 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@v5
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -7,6 +7,9 @@ on:
pull_request:
paths:
- 'icons/*'
- 'lab/*'
permissions: {}
jobs:
lint-filenames:
@@ -16,27 +19,54 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: icons/*
files: |
icons/*
lab/*
- name: Generate annotations
run: node ./scripts/lintFilenames.mts
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
check-lab-icons:
name: Check Lab Icons
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check lab icons do not already exist in icons
run: node ./scripts/checkLabIcons.mts
lint-aliases:
name: Check Uniqueness of Aliases
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check Uniqueness of Aliases
run: "! cat <(printf \"%s\\n\" icons/*.json | while read -r name; do basename \"$name\" .json; done) <(jq -cr 'select(.aliases) | .aliases[] | if type==\"string\" then . else .name end' icons/*.json) | sort | uniq -c | grep -ve '^\\s*1 '"

View File

@@ -11,13 +11,16 @@ on:
- tools/build-icons/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: 'package.json'
cache: 'pnpm'
@@ -31,9 +34,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3.8.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: 'package.json'
cache: 'pnpm'

View File

@@ -9,13 +9,16 @@ on:
- tools/build-font/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
lucide-font:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -30,7 +33,7 @@ jobs:
run: pnpm build:font
- name: 'Upload to Artifacts'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: lucide-font
path: lucide-font

30
.github/workflows/lucide-lab.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Lucide Lab checks
on:
pull_request:
paths:
- packages/lab/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @lucide/lab build

View File

@@ -12,13 +12,16 @@ on:
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
lucide-preact:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -12,13 +12,16 @@ on:
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -32,9 +35,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,13 +13,16 @@ on:
- scripts/generateNextJSAliases.mjs
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -33,9 +36,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -9,13 +9,16 @@ on:
- packages/shared/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -12,13 +12,16 @@ on:
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -32,9 +35,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -7,13 +7,16 @@ on:
- tools/build-icons/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
lucide-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -12,13 +12,16 @@ on:
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -32,9 +35,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -48,9 +51,9 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -6,19 +6,22 @@ on:
- next
pull_request:
paths:
- packages/lucide-vue/**
- packages/vue/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -32,9 +35,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -11,13 +11,16 @@ on:
- tools/rollup-plugins/**
- pnpm-lock.yaml
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -31,9 +34,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -5,27 +5,30 @@ on:
paths:
- 'icons/*.svg'
permissions: {}
jobs:
generate-changed-icons-comment:
name: Generate Changed Icons Comment
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@v46
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: icons/*.svg
- uses: actions/setup-node@v6
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
- name: Install svgson for code preview (safer and faster than installing all deps)
run: npm install svgson@5.3.1 --force
@@ -40,7 +43,7 @@ jobs:
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pr_number
path: pr/

View File

@@ -5,9 +5,12 @@ on:
paths:
- 'icons/*.json'
permissions: {}
jobs:
pull-request-metadata-suggestions:
name: Pull Request Metadata Suggestions
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -16,11 +19,11 @@ jobs:
steps:
# We checkout the main branch of main repository for security reasons.
# This is to prevent the workflow from running on a forked repository.
- uses: actions/checkout@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: lucide-icons/lucide
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -16,9 +16,7 @@ on:
default: 'latest'
type: string
permissions:
id-token: write # Required for OIDC
contents: write
permissions: {}
jobs:
prepare:
@@ -26,7 +24,6 @@ jobs:
runs-on: ubuntu-latest
outputs:
VERSION: ${{ steps.get_version.outputs.VERSION }}
steps:
- name: Get the version
id: get_version
@@ -60,9 +57,9 @@ jobs:
'@lucide/vue',
]
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -93,10 +90,14 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: lucide-font
path: lucide-font
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -122,10 +123,12 @@ jobs:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
needs: prepare
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -142,7 +145,7 @@ jobs:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
- name: 'Upload to Artifacts'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: lucide-font
path: lucide-font
@@ -152,18 +155,21 @@ jobs:
runs-on: ubuntu-latest
needs: [prepare, lucide-font]
permissions:
id-token: write # Required for OIDC
contents: write
contents: write # Required to upload the release assets
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: lucide-font
path: lucide-font
- name: Zip font and icons
run: |
zip -r lucide-font-${{ needs.prepare.outputs.VERSION }}.zip lucide-font
zip -r lucide-icons-${{ needs.prepare.outputs.VERSION }}.zip icons
- name: Release zip and fonts
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
tag_name: ${{ needs.prepare.outputs.VERSION }}

45
.github/workflows/repo-journal.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Weekly Repo Report
on:
workflow_dispatch:
# Run this workflow every Monday at 07:00 UTC
schedule:
- cron: '0 7 * * 1'
permissions:
contents: read
jobs:
weekly-summary:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
steps:
- name: Set up Node.js environment
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Install Copilot CLI
run: npm install -g @github/copilot
- name: Run the prompt
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
copilot -p "${{ vars.REPO_REPORT_PROMPT }} Write the details to summary.md" --allow-tool='shell(git:*)' --allow-tool=write --no-ask-user
cat summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Post report to Discord (private channel)
env:
DISCORD_WEBHOOK_URL: ${{ secrets.LUCIDE_DISCORD_WEBHOOK }}
run: |
jq -Rs '{
embeds: [{
title: "📊 Weekly Repo Report",
description: (.[0:4096]),
color: 5814783
}]
}' summary.md > payload.json
curl -sS --fail -X POST \
-H "Content-Type: application/json" \
-d @payload.json \
"$DISCORD_WEBHOOK_URL"

View File

@@ -1,25 +1,27 @@
name: 'Request Review'
name: Request Review
on:
pull_request_target:
pull_request:
types: [opened]
paths:
- icons/*.svg
permissions: {}
jobs:
request-review:
if: github.event.pull_request.draft == false
if: github.repository == 'lucide-icons/lucide' && github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: icons/*.svg
- run: |

4
.gitignore vendored
View File

@@ -54,8 +54,10 @@ docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vitepress/data/brandStopwords.json
docs/.vitepress/data/iconPopularity
docs/.vitepress/data/lab/iconNodes
docs/.vitepress/data/lab/iconMetaData.ts
docs/.vitepress/data/lab/iconPopularity
docs/.vercel
docs/.nitro
docs/public/og/**
!docs/public/og/general.png
.gitignore

View File

@@ -1,5 +1,5 @@
{
"cSpell.words": ["devs", "preact", "Preact"],
"cSpell.words": ["devs", "linecap", "linejoin", "preact", "Preact"],
"eslint.enable": true,
"eslint.validate": ["javascript", "svg"],
"svg.preview.background": "editor"

View File

@@ -30,7 +30,7 @@ Read it here: [ICON_GUIDELINES](https://lucide.dev/contribute/icon-design-guide)
#### Lucide Studio
For formatting and adjusting SVG icons, [@jguddas](https://github.com/jguddas) made a great tool called [Lucide Studio](https://studio.lucide.dev/). It is a web-based SVG editor that allows you to edit and adjust icons in the Lucide style. You can use it to create new icons or modify existing ones.
For formatting and adjusting SVG icons, [@jguddas](https://github.com/jguddas) made a great tool called [Lucide Studio](https://studio.lucide.dev/?utm_source=lucide.dev&utm_medium=contributing-guide). It is a web-based SVG editor that allows you to edit and adjust icons in the Lucide style. You can use it to create new icons or modify existing ones.
#### Editor guides

View File

@@ -56,7 +56,6 @@
"kubernetes": "Kubernetes",
"leetcode": "LeetCode",
"leet-code": "LeetCode",
"line": "LINE",
"linkedin": "LinkedIn",
"lua": "Lua",
"mariadb": "MariaDB",
@@ -69,7 +68,7 @@
"nestjs": "NestJS",
"netflix": "Netflix",
"netlify": "Netlify",
"next": "Next.js",
"nextjs": "Next.js",
"nodejs": "Node.js",
"notion": "Notion",
"nostr": "Nostr",
@@ -105,7 +104,8 @@
"shopify": "Shopify",
"skype": "Skype",
"slack": "Slack",
"solid": "SolidJS",
"snapchat": "Snapchat",
"solidjs": "SolidJS",
"soundcloud": "SoundCloud",
"spotify": "Spotify",
"sqlite": "SQLite",
@@ -144,6 +144,5 @@
"xbox": "Xbox",
"yarn": "Yarn",
"youtube": "YouTube",
"zig": "Zig",
"zoom": "Zoom"
"zig": "Zig"
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Emoji",
"icon": "smile"
"icon": "face-slightly-smiling"
}

View File

@@ -0,0 +1,25 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../../data/lab/iconMetaData';
import iconPopularity from '../../../data/lab/iconPopularity';
import iconNodes from '../../../data/lab/iconNodes';
export default eventHandler((event) => {
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
return Object.fromEntries(
Object.entries(iconMetaData).map(([iconName, iconData]) => {
// Remove $schema from the response
delete iconData['$schema'];
return [
iconName,
{
name: iconName,
iconNode: iconNodes[iconName],
popularity: iconPopularity[iconName]?.count ?? 0,
...iconData,
},
];
}),
);
});

View File

@@ -0,0 +1,14 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../../data/lab/iconMetaData';
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
for (const iconDetail in iconMetaData) {
// Remove $schema from the response
delete iconMetaData[iconDetail]['$schema'];
}
return iconMetaData;
});

View File

@@ -0,0 +1,30 @@
import { eventHandler, getQuery, setResponseHeader } from 'h3';
import iconNodes from '../../../data/lab/iconNodes';
type IconNodeWithKeys = [elementName: string, attrs: Record<string, string> & { key?: string }][];
export default eventHandler((event) => {
const query = getQuery(event);
const withUniqueKeys = query.withUniqueKeys === 'true';
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
if (withUniqueKeys) {
return iconNodes;
}
return Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
if (withUniqueKeys) {
return [name, iconNode];
}
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
return [name, attrs];
});
acc[name] = newIconNode;
return acc;
}, {});
});

View File

@@ -101,6 +101,7 @@ export default defineConfig({
},
{ text: 'Packages', link: '/packages' },
{ text: 'Showcase', link: '/showcase' },
{ text: 'Merch', link: 'https://merch.lucide.dev/' },
],
sidebar,
socialLinks: [

View File

@@ -46,21 +46,5 @@
"light": "/company-logos/open-collective-light.svg",
"dark": "/company-logos/open-collective-dark.svg"
}
},
{
"name": "Super",
"url": "https://super.so",
"image": {
"light": "/company-logos/super-light.svg",
"dark": "/company-logos/super-dark.svg"
}
},
{
"name": "Noodle",
"url": "https://noodle.run/",
"image": {
"light": "/company-logos/noodle-light.svg",
"dark": "/company-logos/noodle-dark.svg"
}
}
]

View File

View File

@@ -209,5 +209,24 @@
],
"source": "https://github.com/shx08/strapi-lucide-icons",
"documentation": "https://github.com/shx08/strapi-lucide-icons/blob/main/README.md"
},
{
"name": "lucide-lit",
"description": "Implementation of Lucide icons as Lit web components.",
"icon": "/framework-logos/lit.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/npm/v/@gyeonghokim/lucide-lit",
"href": "https://www.npmjs.com/package/@gyeonghokim/lucide-lit"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/npm/dw/@gyeonghokim/lucide-lit",
"href": "https://www.npmjs.com/package/@gyeonghokim/lucide-lit"
}
],
"source": "https://github.com/GyeongHoKim/lucide-lit",
"documentation": "https://github.com/GyeongHoKim/lucide-lit/blob/main/README.md"
}
]

View File

@@ -24,13 +24,6 @@ const getHeadConfig = ({
defer: '',
},
],
[
'script',
{
src: 'https://media.bitterbrains.com/main.js?from=LUCIDE&type=top',
async: 'true',
},
],
[
'meta',
{

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
interface BackdropProps {
src: string;

View File

@@ -3,7 +3,6 @@ import pathToPoints from './path-to-points';
import { Path, PathProps } from './types';
export const GapViolationHighlight = ({
radius,
stroke,
strokeWidth,
strokeOpacity,

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import { PathProps, Path } from './types';
import getPaths, { assert } from './utils';
import { GapViolationHighlight } from './GapViolationHighlight.tsx';
@@ -293,7 +293,7 @@ const Radii = ({
...props
}: { paths: Path[] } & PathProps<
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
any
never
>) => {
return (
<g
@@ -361,12 +361,12 @@ const Radii = ({
const Handles = ({
paths,
...props
}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', any>) => (
}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', never>) => (
<g
className="svg-preview-handles-group"
{...props}
>
{paths.map(({ c, prev, next, cp1, cp2 }, i) => (
{paths.map(({ prev, next, cp1, cp2 }, i) => (
<React.Fragment key={i}>
{cp1 && <path d={`M${prev.x} ${prev.y} ${cp1.x} ${cp1.y}`} />}
{cp1 && (

View File

@@ -12,7 +12,9 @@ function pathToPoints({ d, prev, next }: Path, interval = 1) {
points.push(commander.getPointAtLength(i));
}
points.push(next);
} catch (err) {}
} catch (err) {
console.warn(`Failed to convert path to points for d="${d}": ${err}`);
}
return points;
}

View File

@@ -55,7 +55,7 @@ export const getCommands = (src: string) =>
.flatMap(({ d, name }, idx) =>
new SVGPathData(d)
.toAbs()
// @ts-ignore
// @ts-expect-error `commands` is typed as a union of command types svgson does not narrow
.commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
);
@@ -242,7 +242,7 @@ const getPaths = (src: string) => {
break;
}
default: {
// @ts-ignore
// @ts-expect-error every command type is handled above, so `c` is not `never` here
assertNever(c);
}
}

View File

@@ -13,11 +13,11 @@ const getIconCodes = (): CodeExampleType => {
title: 'Vanilla',
code: `\
<script>
import { createIcons, $CamelCase } from 'lucide';
import { createIcons, $PascalCase } from 'lucide';
createIcons({
icons: {
$CamelCase
$PascalCase
}
});
</script>

View File

@@ -9,11 +9,9 @@ export const hash = (string: string, seed = 5381) => {
let i = string.length;
while (i) {
// eslint-disable-next-line no-bitwise, no-plusplus
seed = (seed * 33) ^ string.charCodeAt(--i);
}
// eslint-disable-next-line no-bitwise
return (seed >>> 0).toString(36).substr(0, 6);
};

View File

@@ -1,5 +1,6 @@
import type MarkdownIt from 'markdown-it';
import type { RenderRule } from 'markdown-it/lib/renderer.mjs';
import type Token from 'markdown-it/lib/token.mjs';
import container from 'markdown-it-container';
import sandpackTheme from '../theme/sandpackTheme.json';
@@ -21,14 +22,10 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
throw new Error('MarkdownIt instance is required for sandpackPlugin');
}
const escapeHtml = md?.utils?.escapeHtml;
const defaultFence =
md.renderer.rules.fence ||
((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
const renderSandbox = (tokenList: any[], index: number) => {
const renderFunc = (tokens: any[], idx: number) => {
const renderSandbox = (tokenList: Token[], index: number) => {
const renderFunc = (tokens: Token[], idx: number) => {
if (tokens[idx].nesting === 1) {
const fileAttr: string[] = [];
const attrs = Object.fromEntries(tokens[idx].attrs || []);
const files: Record<
@@ -47,7 +44,7 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
) {
if (tokens[i].type === 'fence' && tokens[i].tag === 'code') {
const info = tokens[i].info ?? '';
const [lang, fileName, params = ''] = info.split(' ');
const [, fileName, params = ''] = info.split(' ');
const active = params.includes('[active]');
const hidden = params.includes('[hidden]');
@@ -57,15 +54,9 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
if (fileName && code) {
files[fileName] = {
code,
...(active && { active: true }),
...(hidden && { hidden: true }),
};
if (active) {
(files[fileName] as any).active = true;
}
if (hidden) {
(files[fileName] as any).hidden = true;
}
}
}
}
@@ -122,7 +113,7 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
return renderFunc(tokenList, index);
};
function createCodeGroup(md: MarkdownIt): ContainerArgs {
function createCodeGroup(): ContainerArgs {
return [
container,
'sandpack',
@@ -134,5 +125,5 @@ export default function sandpackPlugin(md: MarkdownIt, pluginOptions: SnackParam
];
}
md.use(...createCodeGroup(md));
md.use(...createCodeGroup());
}

View File

@@ -73,7 +73,6 @@ export default function snackPlayerPlugin(md: MarkdownIt) {
'react-native-safe-area-context' + (params.dependencies ? `,${params.dependencies}` : '');
const platform = params.platform ?? 'web';
const supportedPlatforms = params.supportedPlatforms ?? 'ios,android,web';
const theme = params.theme ?? 'light';
const preview = params.preview ?? 'true';
const loading = params.loading ?? 'lazy';
const deviceAppearance = params.deviceAppearance ?? 'dark';
@@ -88,7 +87,7 @@ export default function snackPlayerPlugin(md: MarkdownIt) {
` data-snack-dependencies="${escapeHtml(dependencies)}"` +
` data-snack-platform="${escapeHtml(platform)}"` +
` data-snack-supported-platforms="${escapeHtml(supportedPlatforms)}"` +
// ` data-snack-theme="${escapeHtml(theme)}"` +
// ` data-snack-theme="${escapeHtml(params.theme ?? 'light')}"` +
` data-snack-preview="${escapeHtml(preview)}"` +
` data-snack-loading="${escapeHtml(loading)}"` +
` data-snack-device-appearance="${escapeHtml(deviceAppearance)}"` +

View File

@@ -72,6 +72,11 @@ export const reactNativeSidebar = [
link: '/guide/react-native/advanced/combining-icons',
desc: 'Combine multiple icons into one',
},
{
text: 'Optimizations',
link: '/guide/react-native/advanced/optimizations',
desc: 'Optimizing your app for performance',
},
],
},
{

View File

@@ -1,90 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vitepress';
import { computed } from 'vue';
const router = useRouter();
const frameworks = [
{
ariaLabel: 'Get JavaScript certificates from certificates.dev',
image: '/certificates-dev/js-certificates-dev.jpg',
href: 'https://certificates.dev/javascript?friend=LUCIDE',
routes: [
'/guide/lucide/',
'/guide/svelte/',
'/guide/solid/',
'/guide/astro/',
'/guide/static/'
],
},
{
ariaLabel: 'Get React certificates from certificates.dev',
image: '/certificates-dev/react-certificates-dev.jpg',
href: 'https://certificates.dev/react?friend=LUCIDE',
routes: [
'/guide/react/',
'/guide/react-native/',
'/guide/preact/',
],
},
{
ariaLabel: 'Get Vue certificates from certificates.dev',
href: 'https://certificates.dev/vuejs?friend=LUCIDE',
image: '/certificates-dev/vue-certificates-dev.jpg',
routes: [
'/guide/vue/',
],
},
{
ariaLabel: 'Get Angular certificates from certificates.dev',
href: 'https://certificates.dev/angular?friend=LUCIDE',
image: '/certificates-dev/angular-certificates-dev.jpg',
routes: [
'/guide/angular/',
],
}
];
const selected = computed(() => {
const current = frameworks.find(({ routes }) => {
return routes.some(route => router.route.path.startsWith(route));
});
return current || frameworks[0];
});
</script>
<template>
<a class="certificates-link" :href="selected.href" :aria-label="selected.ariaLabel" target="_blank" rel="noopener noreferrer">
<img :src="selected.image" :alt="selected.ariaLabel" />
</a>
</template>
<style scoped>
.certificates-link {
/* padding: 20px; */
border-radius: 12px;
min-height: 100px;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 500;
background-color: var(--vp-carbon-ads-bg-color);
margin-bottom: 20px;
overflow: hidden;
}
.certificates-link img {
width: 100%;
height: 100%;
max-width: 100%;
border-radius: 6px;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.certificates-link:hover img {
transform: scale(1.05);
opacity: 0.9;
}
</style>

View File

@@ -13,23 +13,21 @@ const { go } = useRouter()
<HomeSectionTitle>Available For:</HomeSectionTitle>
<div class="packages-list">
<a
v-for="{ name, logo, logoDark } in data.packages"
:href="`/guide/packages/${name}`"
v-for="{ name, label, path, logo, logoDark } in data.packages"
:href="path"
class="package-logo"
:aria-label="`Read more about: ${name} package`"
@click.prevent="go(`/guide/packages/${name}`)"
@click.prevent="go(path)"
>
<img
:src="logo"
:class="{ light: logoDark, 'image-logo': true }"
:alt="`${name} logo`"
:alt="label"
loading="lazy"
/>
<img
v-if="logoDark"
:src="logoDark"
:alt="`${name} logo`"
:alt="label"
class="image-logo dark"
loading="lazy"
/>

View File

@@ -1,4 +1,3 @@
import { getAllData } from '../../../lib/icons';
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
import iconsMetaData from '../../../data/iconMetaData';

View File

@@ -3,6 +3,7 @@ import type { IconEntity } from '../../types';
import { computed, ref } from 'vue';
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
import { useIconStyleContext } from '../../composables/useIconStyle';
import { squarePen } from '../../../data/iconNodes';
const props = defineProps<{
name: IconEntity['name'];
@@ -21,6 +22,31 @@ const iconComponent = computed(() => {
if (!props.name || !props.iconNode) return null;
return createLucideIcon(props.name, props.iconNode);
});
const SquarePenIcon = createLucideIcon('SquarePen', squarePen);
const innerSvg = computed(() => {
if (!props.iconNode) return '';
const string = props.iconNode
.map(([tag, attrs]) => {
const attributes = Object.entries(attrs)
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
return `<${tag} ${attributes}/>`;
})
.join('');
return string
.replace(/>[\r\n ]+</g, '><')
.replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
.trim();
});
const base64InnerSvg = computed(() => {
if (typeof window === 'undefined') return '';
return btoa(innerSvg.value);
});
</script>
<template>
@@ -73,6 +99,16 @@ const iconComponent = computed(() => {
100%
</button>
</div>
<a
:href="`https://studio.lucide.dev/edit?value=${encodeURIComponent(base64InnerSvg)}&name=${name}&utm_source=lucide.dev&utm_medium=icon-detail-preview`"
target="_blank"
rel="noopener noreferrer"
class="studio-button"
title="Open in Lucide Studio"
aria-label="Open in Lucide Studio"
>
<SquarePenIcon :size="14" />
</a>
</div>
</template>
@@ -99,6 +135,8 @@ const iconComponent = computed(() => {
background: var(--vp-c-bg-alt);
border-radius: 8px;
display: flex;
overflow: hidden;
flex-shrink: 0;
}
.icon-container:hover .size-buttons {
@@ -131,6 +169,34 @@ const iconComponent = computed(() => {
height: 24px;
}
.studio-button {
position: absolute;
bottom: 8px;
right: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-2);
opacity: 0;
transition: opacity 0.2s, color 0.2s, background-color 0.2s;
z-index: 2;
}
.icon-container:hover .studio-button,
.studio-button:focus-visible {
opacity: 1;
}
.studio-button:hover,
.studio-button:focus-visible {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.icon-component.customizable {
will-change: width, height, stroke-width, stroke;
}

View File

@@ -44,8 +44,12 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories();
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categoriesMap,
isFetching: isFetchingCategories,
} = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -117,6 +121,9 @@ const categoriesList = computed(() => {
}, []);
});
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && (tags.value == null || categoriesMap.value == null),
);
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
itemHeight: ICON_SIZE + ICON_GRID_GAP,
@@ -135,15 +142,21 @@ onMounted(() => {
useEventListener(window, 'scroll', containerProps.onScroll);
});
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
}
if (categoriesMap.value == null) {
fetchCategories();
if (categoriesMap.value == null && !isFetchingCategories.value) {
void fetchCategories();
}
}
watch(searchQuery, (searchString) => {
if (searchString !== '') {
loadSearchMetadata();
}
});
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
@@ -178,11 +191,11 @@ function handleCloseDrawer() {
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
@focus="loadSearchMetadata"
/>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults"
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"

View File

@@ -51,8 +51,12 @@ const props = defineProps<{
const activeIconName = ref(null);
const selectedSort = ref(SORTING[0])
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categories,
isFetching: isFetchingCategories,
} = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -109,6 +113,9 @@ const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
]);
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && (tags.value == null || categories.value == null),
);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
@@ -134,15 +141,21 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
}
if (categories.value == null) {
fetchCategories();
if (categories.value == null && !isFetchingCategories.value) {
void fetchCategories();
}
}
watch(searchQuery, (searchString) => {
if (searchString !== '') {
loadSearchMetadata();
}
});
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
@@ -178,7 +191,7 @@ function handleCloseDrawer() {
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@focus="onFocusSearchInput"
@focus="loadSearchMetadata"
/>
<Select
@@ -196,7 +209,7 @@ function handleCloseDrawer() {
</Select>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults"
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"

View File

@@ -16,7 +16,7 @@ export const externalLibContext = {
};
const externalLibIconNodesAPI = {
lab: 'https://lab.lucide.dev/api/icon-details',
lab: `${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/lab/icon-details`,
};
export function useExternalLibs(): ExternalLibContext {
@@ -32,13 +32,15 @@ export function useExternalLibs(): ExternalLibContext {
newExternalIconNodes[lib] = savedIconNodes[lib];
} else {
const response = await fetch(externalLibIconNodesAPI[lib]);
const iconNodes = await response.json();
const iconDetails = await response.json();
if (iconNodes) {
newExternalIconNodes[lib] = Object.values(iconNodes).map((iconEntity: IconEntity) => ({
...iconEntity,
externalLibrary: lib,
}));
if (iconDetails) {
newExternalIconNodes[lib] = Object.values(iconDetails).map((iconEntity: IconEntity) => {
return {
...iconEntity,
externalLibrary: lib,
};
});
}
}
}

View File

@@ -1,5 +1,3 @@
/* eslint-disable no-console */
import { ref, inject, Ref } from 'vue';
export const ICON_STYLE_CONTEXT = Symbol('style');

View File

@@ -12,7 +12,6 @@ import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCat
import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs';
import FrameworkSelect from './components/guide/FrameworkSelect.vue';
import SnackPlayer from './components/editors/SnackPlayer.vue';
import CertificatesLink from './components/guide/CertificatesLink.vue';
const theme: Partial<Theme> = {
extends: DefaultTheme,
@@ -23,7 +22,6 @@ const theme: Partial<Theme> = {
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
'home-hero-image': () => h(HomeHeroIconsCard),
'home-hero-actions-after': () => h(HomeHeroAfter),
'aside-ads-before': () => h(CertificatesLink),
// Keep this here for now, we might want to add more things to the top bar in the future
// 'layout-top': () => h(LayoutTop),
});

View File

@@ -67,6 +67,10 @@
animation: .3s transition 0s ease;
} */
html {
scrollbar-gutter: stable;
}
.VPHome {
overflow-x: hidden;
}
@@ -148,14 +152,6 @@
.VPHomeHero .container {
flex-direction: row;
}
html.has-bb-banner {
--vp-layout-top-height: 72px;
}
html.has-bb-banner {
--Dark-Background: #161618;
}
}
.VPNavBarHamburger .container > span {
@@ -167,12 +163,6 @@
max-width: 480px;
}
html:not(.has-bb-banner) #bb-banner {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
.VPSidebar {
/* Hide Scrollbar */
-ms-overflow-style: none;

View File

@@ -1,4 +1,4 @@
import { PageData } from 'vitepress';
import { type PageData } from 'vitepress';
import getStructuredData from './getStructuredData';
import { createGeneralOGImage, createIconOGImage } from './createOGImage';

View File

@@ -6,13 +6,13 @@ declare module '*.vue' {
}
declare module '*.data.ts' {
const data: any;
const data: unknown;
export { data };
}
declare module '*.data' {
const data: any;
const data: unknown;
export { data };
}
@@ -75,7 +75,7 @@ declare global {
doNotShowAfterSubmit?: boolean;
customFormUrl?: string; // when you want to load the form via it's custom domain URL
hiddenFields?: {
[key: string]: any;
[key: string]: unknown;
};
onOpen?: () => void;
onClose?: () => void;

View File

@@ -158,7 +158,7 @@ For each icon these attributes are applied, corresponding to the above rules.
### Minify paths
The code of paths can sometimes get quite large. To reduce file size we like to minify the code.
We recommend to use [Lucide Studio](https://studio.lucide.dev/) to tidy paths to 3 points of precision.
We recommend to use [Lucide Studio](https://studio.lucide.dev/?utm_source=lucide.dev&utm_medium=design-guide) to tidy paths to 3 points of precision.
### Allowed elements

View File

@@ -12,6 +12,10 @@ import Sandpack from '~/.vitepress/theme/components/editors/SandpackAngular.vue'
While they aren't provided as standalone components, they can be still be passed to the `LucideIcon` component the same way as official icons:
::: info Limitation
Only custom icons that follow Lucide's [code conventions](../../../contribute/icon-design-guide.md#code-conventions) are supported.
:::
### Directly as LucideIconData
::: sandpack {template=angular showTabs=false editorHeight=400 editorWidthPercentage=60 dependencies="@lucide/angular,@lucide/lab"}

View File

@@ -0,0 +1,56 @@
---
title: Optimizations - React Native
description: Learn how to optimize the bundle size of your React Native and Expo web exports by tree-shaking Lucide icons.
---
# Optimizations
By default you import icons from the `lucide-react-native` entry point:
```jsx
import { Camera } from 'lucide-react-native';
```
This is convenient, but it relies on the bundler to _tree-shake_ the entry point (a "barrel" file that re-exports every icon) so that only the icons you actually use end up in your bundle.
On native platforms this is usually fine. On **web exports** it can be a problem, because the Metro bundler is currently not very good at tree-shaking barrel imports and may include **all** icons in your final bundle.
## The Metro tree-shaking limitation
When you build for web with Expo (`expo export --platform web`), Metro is used to bundle your JavaScript. Metro bundles modules on-demand and, by default, does not remove unused exports from a barrel file.
This means an import like the one below can pull in the entire icon set, even though only a single icon is rendered:
```jsx
// May bundle every Lucide icon on web exports
import { Camera } from 'lucide-react-native';
```
Expo documents an experimental optimization for [React Native web imports](https://docs.expo.dev/guides/tree-shaking/#react-native-web-imports) and for [removing unused imports and exports](https://docs.expo.dev/guides/tree-shaking/#remove-unused-imports-and-exports). In practice this tree-shaking does **not** currently work reliably for `lucide-react-native` barrel imports, so the unused icons are not removed from web exports.
## Recommended: import icons individually
To guarantee that only the icons you use are bundled — regardless of the bundler's tree-shaking capabilities — import each icon directly from its own module:
```jsx
import Camera from 'lucide-react-native/icons/camera';
// Usage
const App = () => {
return <Camera />;
};
export default App;
```
Because each icon lives in its own file, the bundler only ever includes the icons you explicitly import. This keeps your web export small without depending on Metro's experimental tree-shaking.
The icon module name is the [kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) version of the icon name. For example, the `ArrowRight` icon is imported from `lucide-react-native/icons/arrow-right`:
```jsx
import ArrowRight from 'lucide-react-native/icons/arrow-right';
```
::: tip
Individual icon imports work identically on native and web, so you can safely use this style across your whole project to keep bundles as small as possible on every platform.
:::

View File

@@ -1,24 +1,26 @@
import { IconEntity } from '../../.vitepress/theme/types';
import iconNodes from '../../.vitepress/data/lab/iconNodes';
import iconPopularity from '../../.vitepress/data/lab/iconPopularity';
import iconMetaData from '../../.vitepress/data/lab/iconMetaData';
import { type IconEntity } from '../../.vitepress/theme/types';
export default {
paths: async () => {
try {
const iconDetailsResponse = await fetch('https://lab.lucide.dev/api/icon-details');
const iconDetails = (await iconDetailsResponse.json()) as Record<string, IconEntity>;
return (Object.entries(iconNodes) as unknown as IconEntity[]).map(([name, iconNode]) => {
const iconDetails = iconMetaData[name];
return Object.values(iconDetails).map((iconEntity) => {
const params = {
externalLibrary: 'lab',
...iconEntity,
};
delete iconDetails['$schema'];
return {
params,
};
});
} catch (error) {
console.error('Error fetching icon details:', error);
return [];
}
const params = {
name,
iconNode,
externalLibrary: 'lab',
popularity: iconPopularity[name]?.count ?? 0,
...iconDetails,
};
return {
params,
};
});
},
};

View File

@@ -8,19 +8,21 @@
"docs:dev": "pnpm run /^prebuild:.*/ && vitepress dev",
"docs:build": "pnpm run /^prebuild:.*/ && vitepress build",
"docs:preview": "vitepress preview",
"build:docs": "vitepress build",
"build:docs": "NODE_OPTIONS=--max-old-space-size=8192 vitepress build",
"prebuild:iconNodes": "node ./scripts/writeIconNodes.mts",
"prebuild:metaJson": "node ./scripts/writeIconMetaIndex.mts",
"prebuild:releaseJson": "node ./scripts/writeReleaseMetadata.mts",
"prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mts",
"prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mts",
"prebuild:iconDetails": "node ./scripts/writeIconDetails.mts",
"prebuild:iconPopularity": "node ./scripts/writeiconPopularity.mts",
"prebuild:iconPopularity": "node ./scripts/writeiconPopularity.mts && node ./scripts/writeiconPopularityIndex.mts",
"prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mts",
"prebuild:writeLabIconNodes": "node ./scripts/writeLabIconNodes.mts",
"prebuild:writeLabIconMetaIndex": "node ./scripts/writeLabIconMetaIndex.mts",
"postbuild:vercelJson": "node ./scripts/writeVercelOutput.mts",
"dev": "npx nitropack dev",
"prebuild:api": "npx nitropack prepare",
"build:api": "npx nitropack build",
"dev": "nitropack dev",
"prebuild:api": "nitropack prepare",
"build:api": "NODE_OPTIONS=--max-old-space-size=8192 nitropack build",
"build": "pnpm run /^prebuild:.*/ && pnpm run build:api && pnpm run build:docs && pnpm postbuild:vercelJson",
"preview": "node .output/server/index.mjs"
},
@@ -32,31 +34,32 @@
"@lucide/shared": "workspace:*",
"@rollup/plugin-replace": "^6.0.3",
"@types/semver": "^7.7.1",
"h3": "^1.15.9",
"h3": "^1.15.11",
"markdown-it": "^14.3.0",
"markdown-it-container": "^4.0.0",
"nitropack": "^2.13.4",
"rollup-plugin-copy": "^3.5.0",
"svg-path-commander": "^2.1.11",
"vitepress": "^1.6.4",
"vitepress-plugin-group-icons": "^1.6.5",
"vitepress-plugin-llms": "^1.11.1",
"markdown-it": "^14.2.0",
"markdown-it-container": "^4.0.0",
"semver": "^7.7.3",
"semver": "^7.8.5",
"sharp": "^0.34.5",
"shiki": "^3.15.0",
"shiki": "^3.23.0",
"simple-git": "^3.36.0",
"sitemap": "^7.1.2",
"sitemap": "^7.1.3",
"svg-path-commander": "^2.2.1",
"svg-pathdata": "^6.0.3",
"svgson": "^5.3.1"
"svgson": "^5.3.1",
"vitepress": "^1.6.4",
"vitepress-plugin-group-icons": "^1.7.5",
"vitepress-plugin-llms": "^1.13.3"
},
"dependencies": {
"@floating-ui/vue": "^1.1.9",
"@floating-ui/vue": "^1.1.11",
"@headlessui/vue": "^1.7.23",
"@lucide/vue": "workspace:*",
"@vueuse/components": "^14.0.0",
"@vueuse/core": "^14.0.0",
"@vueuse/components": "^14.3.0",
"@vueuse/core": "^14.3.0",
"@vueuse/integrations": "^14.3.0",
"element-to-path": "^1.2.1",
"fuse.js": "^7.1.0",
"fuse.js": "^7.5.0",
"jszip": "^3.10.1",
"lodash-es": "^4.18.1",
"lucide": "workspace:*",
@@ -65,6 +68,6 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"sandpack-vue3": "3.1.12",
"vue": "^3.5.24"
"vue": "^3.5.41"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -1,28 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="10 -10 304 120">
<g clip-path="url(#a)">
<g clip-path="url(#b)">
<path fill="url(#c)" d="M72 0H28A28 28 0 0 0 0 28v44a28 28 0 0 0 28 28h44a28 28 0 0 0 28-28V28A28 28 0 0 0 72 0Z"/>
<path stroke="#161616" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m47.81 54.34 26.63-6.66-19.73-19.1-26.63 6.67 19.73 19.1Z"/>
<path stroke="#161616" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m47.81 54.34 18.23-4.56c.67 5.86-.29 11.8-2.76 17.15a31.87 31.87 0 0 0-19.65 3.01 31.87 31.87 0 0 0-15.5-12.43 32.2 32.2 0 0 1 6.18-16.23l13.5 13.06Z"/>
<path stroke="#161616" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m33.37 67.04 5.17-19.32 11.84-2.96m-2.57 9.58 26.63-6.66-19.73-19.1-26.63 6.67 19.73 19.1Zm0 0 18.23-4.56c.67 5.86-.29 11.8-2.76 17.15a31.87 31.87 0 0 0-19.65 3.01 31.87 31.87 0 0 0-15.5-12.43 32.2 32.2 0 0 1 6.18-16.23l13.5 13.06Z"/>
</g>
<path fill="#161616" d="M281.5 67.51c-2.63 0-4.9-.54-6.8-1.64a11.08 11.08 0 0 1-4.39-4.67 15.64 15.64 0 0 1-1.53-7.14c0-2.7.51-5.05 1.53-7.09a11.6 11.6 0 0 1 4.33-4.77 12.14 12.14 0 0 1 6.53-1.72c1.62 0 3.14.26 4.57.78 1.44.51 2.72 1.3 3.82 2.39a11.18 11.18 0 0 1 2.62 4.12c.64 1.66.96 3.64.96 5.93v1.9h-21.46v-4.16h15.54a6.63 6.63 0 0 0-.76-3.16 5.58 5.58 0 0 0-2.1-2.2 5.9 5.9 0 0 0-3.1-.8c-1.26 0-2.37.31-3.33.92-.95.6-1.7 1.4-2.23 2.4a6.88 6.88 0 0 0-.8 3.21v3.63c0 1.53.28 2.83.83 3.92a5.96 5.96 0 0 0 2.34 2.5c1 .56 2.17.84 3.51.84.9 0 1.71-.12 2.44-.37a5.4 5.4 0 0 0 1.89-1.14c.53-.5.94-1.12 1.21-1.86l5.76.65a9 9 0 0 1-2.08 3.99c-1 1.12-2.3 2-3.88 2.62-1.58.61-3.39.92-5.42.92Z"/>
<path fill="#161616" d="M263.56 32.1V67h-6.17V32.1h6.17Z"/>
<path fill="#161616" d="M236.87 67.46a9.9 9.9 0 0 1-5.52-1.58 10.77 10.77 0 0 1-3.85-4.6 17.18 17.18 0 0 1-1.42-7.34c0-2.9.48-5.36 1.43-7.36a10.6 10.6 0 0 1 3.9-4.55 10.03 10.03 0 0 1 5.48-1.55c1.53 0 2.8.26 3.78.78a7.13 7.13 0 0 1 2.35 1.86c.58.72 1.03 1.4 1.35 2.03h.26V32.09h6.18V67h-6.06v-4.13h-.38c-.32.64-.78 1.32-1.38 2.03a7.6 7.6 0 0 1-6.12 2.56Zm1.72-5.06c1.3 0 2.42-.35 3.34-1.06a6.6 6.6 0 0 0 2.1-2.98c.48-1.27.72-2.76.72-4.45 0-1.7-.24-3.17-.72-4.42a6.28 6.28 0 0 0-2.08-2.91 5.38 5.38 0 0 0-3.36-1.04c-1.37 0-2.52.36-3.44 1.07a6.5 6.5 0 0 0-2.08 2.97c-.47 1.26-.7 2.7-.7 4.33 0 1.64.23 3.1.7 4.38a6.84 6.84 0 0 0 2.1 3.02 5.4 5.4 0 0 0 3.42 1.09Z"/>
<path fill="#161616" d="M209.37 67.51c-2.56 0-4.77-.56-6.65-1.69a11.46 11.46 0 0 1-4.36-4.72 15.52 15.52 0 0 1-1.53-7.09c0-2.7.5-5.07 1.53-7.1a11.43 11.43 0 0 1 4.36-4.75 12.68 12.68 0 0 1 6.65-1.68c2.56 0 4.78.56 6.65 1.68a11.32 11.32 0 0 1 4.35 4.74 15.47 15.47 0 0 1 1.55 7.11c0 2.7-.52 5.07-1.55 7.1a11.34 11.34 0 0 1-4.35 4.71 12.68 12.68 0 0 1-6.65 1.7Zm.04-4.94c1.38 0 2.54-.38 3.47-1.14a6.89 6.89 0 0 0 2.08-3.1c.47-1.3.7-2.74.7-4.34 0-1.6-.23-3.05-.7-4.34a6.88 6.88 0 0 0-2.08-3.12 5.27 5.27 0 0 0-3.47-1.16c-1.42 0-2.6.39-3.55 1.16a7 7 0 0 0-2.1 3.12c-.45 1.3-.68 2.74-.68 4.34 0 1.6.23 3.04.68 4.33a7 7 0 0 0 2.1 3.1 5.46 5.46 0 0 0 3.55 1.15Z"/>
<path fill="#161616" d="M180.17 67.51c-2.56 0-4.77-.56-6.65-1.69a11.46 11.46 0 0 1-4.36-4.72 15.52 15.52 0 0 1-1.54-7.09c0-2.7.51-5.07 1.54-7.1a11.43 11.43 0 0 1 4.36-4.75 12.68 12.68 0 0 1 6.65-1.68c2.56 0 4.77.56 6.65 1.68a11.32 11.32 0 0 1 4.34 4.74 15.49 15.49 0 0 1 1.55 7.11c0 2.7-.51 5.07-1.55 7.1a11.34 11.34 0 0 1-4.34 4.71 12.68 12.68 0 0 1-6.65 1.7Zm.03-4.94c1.39 0 2.55-.38 3.48-1.14a6.89 6.89 0 0 0 2.08-3.1c.47-1.3.7-2.74.7-4.34 0-1.6-.23-3.05-.7-4.34a6.88 6.88 0 0 0-2.08-3.12 5.26 5.26 0 0 0-3.48-1.16c-1.42 0-2.6.39-3.54 1.16a7 7 0 0 0-2.1 3.12c-.45 1.3-.68 2.74-.68 4.34 0 1.6.23 3.04.68 4.33a7 7 0 0 0 2.1 3.1 5.46 5.46 0 0 0 3.54 1.15Z"/>
<path fill="#161616" d="M162.13 32.1V67h-5.62l-16.45-23.78h-.3V67h-6.32V32.1h5.66l16.43 23.79h.31v-23.8h6.3Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#161616" d="M0 0h293.14v100H0z"/>
</clipPath>
<clipPath id="b">
<path fill="#161616" d="M0 0h100v100H0z"/>
</clipPath>
<linearGradient id="c" x1="0" x2="100" y1="50" y2="50" gradientUnits="userSpaceOnUse">
<stop stop-color="#F77062"/>
<stop offset="1" stop-color="#FE5196"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,28 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="10 -10 304 120">
<g clip-path="url(#a)">
<g clip-path="url(#b)">
<path fill="url(#c)" d="M72 0H28A28 28 0 0 0 0 28v44a28 28 0 0 0 28 28h44a28 28 0 0 0 28-28V28A28 28 0 0 0 72 0Z"/>
<path stroke="#161616" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m47.81 54.34 26.63-6.66-19.73-19.1-26.63 6.67 19.73 19.1Z"/>
<path stroke="#161616" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m47.81 54.34 18.23-4.56c.67 5.86-.29 11.8-2.76 17.15a31.87 31.87 0 0 0-19.65 3.01 31.87 31.87 0 0 0-15.5-12.43 32.2 32.2 0 0 1 6.18-16.23l13.5 13.06Z"/>
<path stroke="#161616" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m33.37 67.04 5.17-19.32 11.84-2.96m-2.57 9.58 26.63-6.66-19.73-19.1-26.63 6.67 19.73 19.1Zm0 0 18.23-4.56c.67 5.86-.29 11.8-2.76 17.15a31.87 31.87 0 0 0-19.65 3.01 31.87 31.87 0 0 0-15.5-12.43 32.2 32.2 0 0 1 6.18-16.23l13.5 13.06Z"/>
</g>
<path fill="#fff" d="M281.5 67.51c-2.63 0-4.9-.54-6.8-1.64a11.08 11.08 0 0 1-4.39-4.67 15.64 15.64 0 0 1-1.53-7.14c0-2.7.51-5.05 1.53-7.09a11.6 11.6 0 0 1 4.33-4.77 12.14 12.14 0 0 1 6.53-1.72c1.62 0 3.14.26 4.57.78 1.44.51 2.72 1.3 3.82 2.39a11.18 11.18 0 0 1 2.62 4.12c.64 1.66.96 3.64.96 5.93v1.9h-21.46v-4.16h15.54a6.63 6.63 0 0 0-.76-3.16 5.58 5.58 0 0 0-2.1-2.2 5.9 5.9 0 0 0-3.1-.8c-1.26 0-2.37.31-3.33.92-.95.6-1.7 1.4-2.23 2.4a6.88 6.88 0 0 0-.8 3.21v3.63c0 1.53.28 2.83.83 3.92a5.96 5.96 0 0 0 2.34 2.5c1 .56 2.17.84 3.51.84.9 0 1.71-.12 2.44-.37a5.4 5.4 0 0 0 1.89-1.14c.53-.5.94-1.12 1.21-1.86l5.76.65a9 9 0 0 1-2.08 3.99c-1 1.12-2.3 2-3.88 2.62-1.58.61-3.39.92-5.42.92Z"/>
<path fill="#fff" d="M263.56 32.1V67h-6.17V32.1h6.17Z"/>
<path fill="#fff" d="M236.87 67.46a9.9 9.9 0 0 1-5.52-1.58 10.77 10.77 0 0 1-3.85-4.6 17.18 17.18 0 0 1-1.42-7.34c0-2.9.48-5.36 1.43-7.36a10.6 10.6 0 0 1 3.9-4.55 10.03 10.03 0 0 1 5.48-1.55c1.53 0 2.8.26 3.78.78a7.13 7.13 0 0 1 2.35 1.86c.58.72 1.03 1.4 1.35 2.03h.26V32.09h6.18V67h-6.06v-4.13h-.38c-.32.64-.78 1.32-1.38 2.03a7.6 7.6 0 0 1-6.12 2.56Zm1.72-5.06c1.3 0 2.42-.35 3.34-1.06a6.6 6.6 0 0 0 2.1-2.98c.48-1.27.72-2.76.72-4.45 0-1.7-.24-3.17-.72-4.42a6.28 6.28 0 0 0-2.08-2.91 5.38 5.38 0 0 0-3.36-1.04c-1.37 0-2.52.36-3.44 1.07a6.5 6.5 0 0 0-2.08 2.97c-.47 1.26-.7 2.7-.7 4.33 0 1.64.23 3.1.7 4.38a6.84 6.84 0 0 0 2.1 3.02 5.4 5.4 0 0 0 3.42 1.09Z"/>
<path fill="#fff" d="M209.37 67.51c-2.56 0-4.77-.56-6.65-1.69a11.46 11.46 0 0 1-4.36-4.72 15.52 15.52 0 0 1-1.53-7.09c0-2.7.5-5.07 1.53-7.1a11.43 11.43 0 0 1 4.36-4.75 12.68 12.68 0 0 1 6.65-1.68c2.56 0 4.78.56 6.65 1.68a11.32 11.32 0 0 1 4.35 4.74 15.47 15.47 0 0 1 1.55 7.11c0 2.7-.52 5.07-1.55 7.1a11.34 11.34 0 0 1-4.35 4.71 12.68 12.68 0 0 1-6.65 1.7Zm.04-4.94c1.38 0 2.54-.38 3.47-1.14a6.89 6.89 0 0 0 2.08-3.1c.47-1.3.7-2.74.7-4.34 0-1.6-.23-3.05-.7-4.34a6.88 6.88 0 0 0-2.08-3.12 5.27 5.27 0 0 0-3.47-1.16c-1.42 0-2.6.39-3.55 1.16a7 7 0 0 0-2.1 3.12c-.45 1.3-.68 2.74-.68 4.34 0 1.6.23 3.04.68 4.33a7 7 0 0 0 2.1 3.1 5.46 5.46 0 0 0 3.55 1.15Z"/>
<path fill="#fff" d="M180.17 67.51c-2.56 0-4.77-.56-6.65-1.69a11.46 11.46 0 0 1-4.36-4.72 15.52 15.52 0 0 1-1.54-7.09c0-2.7.51-5.07 1.54-7.1a11.43 11.43 0 0 1 4.36-4.75 12.68 12.68 0 0 1 6.65-1.68c2.56 0 4.77.56 6.65 1.68a11.32 11.32 0 0 1 4.34 4.74 15.49 15.49 0 0 1 1.55 7.11c0 2.7-.51 5.07-1.55 7.1a11.34 11.34 0 0 1-4.34 4.71 12.68 12.68 0 0 1-6.65 1.7Zm.03-4.94c1.39 0 2.55-.38 3.48-1.14a6.89 6.89 0 0 0 2.08-3.1c.47-1.3.7-2.74.7-4.34 0-1.6-.23-3.05-.7-4.34a6.88 6.88 0 0 0-2.08-3.12 5.26 5.26 0 0 0-3.48-1.16c-1.42 0-2.6.39-3.54 1.16a7 7 0 0 0-2.1 3.12c-.45 1.3-.68 2.74-.68 4.34 0 1.6.23 3.04.68 4.33a7 7 0 0 0 2.1 3.1 5.46 5.46 0 0 0 3.54 1.15Z"/>
<path fill="#fff" d="M162.13 32.1V67h-5.62l-16.45-23.78h-.3V67h-6.32V32.1h5.66l16.43 23.79h.31v-23.8h6.3Z"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h293.14v100H0z"/>
</clipPath>
<clipPath id="b">
<path fill="#fff" d="M0 0h100v100H0z"/>
</clipPath>
<linearGradient id="c" x1="0" x2="100" y1="50" y2="50" gradientUnits="userSpaceOnUse">
<stop stop-color="#F77062"/>
<stop offset="1" stop-color="#FE5196"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 151 31">
<path fill="#111" d="M49.37 7.6c-8.39 0-10.49 1.8-11.13 5.39-.62 3.58.93 5.06 8.1 5.06 4.2 0 5.39 0 5.2 1.04-.2 1.06-1.4 1.06-4.4 1.06-3 0-4.19 0-3.96-1.2H36.9c-.74 4.2 1.13 5.4 9.53 5.4 8.39 0 10.68-1.2 11.42-5.4.65-3.58-1.2-5.1-8.39-5.1-4.2 0-5.39-.02-5.2-1.03.2-1.06 1.4-1.06 4.4-1.06 2.38 0 4.19 0 3.96 1.2h5.97c.65-3.69-.83-5.36-9.23-5.36Z"/>
<path fill="#FFC158" d="M.84 15.81C.84 4.45 4.63.66 16 .66c11.37 0 15.15 3.79 15.15 15.15S27.36 30.96 16 30.96C4.63 30.96.84 27.17.84 15.81Z"/>
<path fill="#fff" fill-rule="evenodd" d="m16.16 8.37-5.8 8.87c-.18.28.02.65.36.65h5.32l-.99 5.37c-.08.46.52.7.77.31l5.8-8.86a.42.42 0 0 0-.36-.66H15.94l1-5.37c.08-.46-.52-.7-.78-.31Z" clip-rule="evenodd"/>
<path fill="#111" d="m77.04 8.18-1.38 7.78c-.53 3-2.9 4.2-5.33 4.2s-4.37-1.2-3.82-4.2l1.39-7.78h-6.71l-1.42 7.78c-1.08 5.97 2.14 8.39 7.63 8.39 4.28 0 7.04-1.81 7.04-1.81l.4 1.2h6.08l2.8-15.56h-6.68Z"/>
<path fill="#111" d="M98.83 7.6c-4.27 0-6.41 1.8-6.41 1.8l.23-1.19h-6.7l-4.09 22.75h6.71l1.51-8.39s1.52 1.8 5.8 1.8c5.48 0 10.05-1.8 11.24-8.38 1.15-6.62-2.8-8.4-8.29-8.4Zm1.55 8.36c-.53 3-2.9 4.2-5.33 4.2s-4.37-1.2-3.82-4.2c.53-3 2.9-4.2 5.33-4.2s4.34 1.2 3.82 4.2Z"/>
<path fill="#111" d="M121.08 7.6c-7.3 0-11.67 2.26-12.76 8.23-1.08 5.97 2.47 8.52 9.77 8.52 5.13 0 7.92-.9 10.55-3.23l-3.91-2.49c-.86.52-1.78 1.52-5.89 1.52-2.76 0-4-1.03-3.94-3.13h15.72c.1-.39.2-.77.26-1.2 1.05-6-2.5-8.22-9.8-8.22Zm-5.52 6.42c.78-1.68 2-2.26 4.76-2.26 2.73 0 3.78.55 3.95 2.26h-8.71Z"/>
<path fill="#111" d="m140.33 9.37.23-1.2h-6.7l-2.8 15.56h6.7l1.19-6.58c.76-4.2 3.91-4.78 6.35-4.78 2.4 0 3.19.26 4.76.62l.86-4.78c-.86-.26-2.9-.61-4.77-.61-4.27 0-5.82 1.77-5.82 1.77Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 151 31">
<path fill="#fff" d="M49.37 7.6c-8.39 0-10.49 1.8-11.13 5.39-.62 3.58.93 5.06 8.1 5.06 4.2 0 5.39 0 5.2 1.04-.2 1.06-1.4 1.06-4.4 1.06-3 0-4.19 0-3.96-1.2H36.9c-.74 4.2 1.13 5.4 9.53 5.4 8.39 0 10.68-1.2 11.42-5.4.65-3.58-1.2-5.1-8.39-5.1-4.2 0-5.39-.02-5.2-1.03.2-1.06 1.4-1.06 4.4-1.06 2.38 0 4.19 0 3.96 1.2h5.97c.65-3.69-.83-5.36-9.23-5.36Z"/>
<path fill="#FFC158" d="M.84 15.81C.84 4.45 4.63.66 16 .66c11.37 0 15.15 3.79 15.15 15.15S27.36 30.96 16 30.96C4.63 30.96.84 27.17.84 15.81Z"/>
<path fill="#fff" fill-rule="evenodd" d="m16.16 8.37-5.8 8.87c-.18.28.02.65.36.65h5.32l-.99 5.37c-.08.46.52.7.77.31l5.8-8.86a.42.42 0 0 0-.36-.66H15.94l1-5.37c.08-.46-.52-.7-.78-.31Z" clip-rule="evenodd"/>
<path fill="#fff" d="m77.04 8.18-1.38 7.78c-.53 3-2.9 4.2-5.33 4.2s-4.37-1.2-3.82-4.2l1.39-7.78h-6.71l-1.42 7.78c-1.08 5.97 2.14 8.39 7.63 8.39 4.28 0 7.04-1.81 7.04-1.81l.4 1.2h6.08l2.8-15.56h-6.68Z"/>
<path fill="#fff" d="M98.83 7.6c-4.27 0-6.41 1.8-6.41 1.8l.23-1.19h-6.7l-4.09 22.75h6.71l1.51-8.39s1.52 1.8 5.8 1.8c5.48 0 10.05-1.8 11.24-8.38 1.15-6.62-2.8-8.4-8.29-8.4Zm1.55 8.36c-.53 3-2.9 4.2-5.33 4.2s-4.37-1.2-3.82-4.2c.53-3 2.9-4.2 5.33-4.2s4.34 1.2 3.82 4.2Z"/>
<path fill="#fff" d="M121.08 7.6c-7.3 0-11.67 2.26-12.76 8.23-1.08 5.97 2.47 8.52 9.77 8.52 5.13 0 7.92-.9 10.55-3.23l-3.91-2.49c-.86.52-1.78 1.52-5.89 1.52-2.76 0-4-1.03-3.94-3.13h15.72c.1-.39.2-.77.26-1.2 1.05-6-2.5-8.22-9.8-8.22Zm-5.52 6.42c.78-1.68 2-2.26 4.76-2.26 2.73 0 3.78.55 3.95 2.26h-8.71Z"/>
<path fill="#fff" d="m140.33 9.37.23-1.2h-6.7l-2.8 15.56h6.7l1.19-6.58c.76-4.2 3.91-4.78 6.35-4.78 2.4 0 3.19.26 4.76.62l.86-4.78c-.86-.26-2.9-.61-4.77-.61-4.27 0-5.82 1.77-5.82 1.77Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 200" fill="none">
<path fill="#00e8ff" d="M40 120l20-60l90 90l-30 50l-40-40h-20" />
<path fill="#283198" d="M80 160 L80 80 L120 40 L 120 120 M0 160 L40 200 L40 120 L20 120" />
<path fill="#324fff" d="M40 120v-80l40-40v80M120 200v-80l40-40v80M0 160v-80l40 40" />
<path fill="#0ff" d="M40 200v-80l40 40" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -23,12 +23,12 @@ const writeIconFiles = icons.map(async (iconFileName) => {
const location = path.resolve(iconDetailsDirectory, `${iconName}.ts`);
const contents = `\
import iconNode from '../iconNodes/${iconName}.node.json'
import metaData from '../../../../icons/${iconName}.json'
import releaseData from '../releaseMetadata/${iconName}.json'
import popularity from '../iconPopularity/${iconName}.json'
import iconNode from '../iconNodes/${iconName}.node.json' with { type: 'json' };
import metaData from '../../../../icons/${iconName}.json' with { type: 'json' };
import releaseData from '../releaseMetadata/${iconName}.json' with { type: 'json' };
import popularity from '../iconPopularity/${iconName}.json' with { type: 'json' };
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData;
const iconDetails = {
name: '${iconName}',
@@ -42,9 +42,9 @@ const iconDetails = {
deprecationReason,
toBeRemovedInVersion,
...releaseData,
}
};
export default iconDetails
export default iconDetails;
`;
await fs.promises.writeFile(location, contents, 'utf-8');

View File

@@ -0,0 +1,49 @@
import fs from 'fs';
import path from 'path';
import { readSvgDirectory, toCamelCase } from '@lucide/helpers';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../lab');
const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json');
const labDirectory = path.resolve(currentDir, '.vitepress/data/lab');
const location = path.resolve(currentDir, labDirectory, 'iconMetaData.ts');
if (fs.existsSync(location)) {
fs.unlinkSync(location);
}
if (!fs.existsSync(labDirectory)) {
fs.mkdirSync(labDirectory);
}
const iconMetaIndexFileImports = [];
const iconMetaIndexFileExports = [];
iconJsonFiles.forEach((iconJsonFile) => {
const iconName = path.basename(iconJsonFile, '.json');
iconMetaIndexFileImports.push(
`import ${toCamelCase(iconName)}Metadata from '../../../../lab/${iconName}.json';`,
);
iconMetaIndexFileExports.push(` '${iconName}': ${toCamelCase(iconName)}Metadata,`);
});
try {
await fs.promises.writeFile(
location,
`\
${iconMetaIndexFileImports.join('\n')}
export default {
${iconMetaIndexFileExports.join('\n')}
}
`,
'utf-8',
);
console.log('Successfully write icon json file index');
} catch (error) {
throw new Error(`Something went wrong generating icon json file index file,\n ${error}`);
}

View File

@@ -0,0 +1,62 @@
import fs from 'fs';
import path from 'path';
import { renderIconsObject } from '@lucide/build-icons';
import { readSvgDirectory, toCamelCase } from '@lucide/helpers';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../lab');
const svgFiles = await readSvgDirectory(ICONS_DIR);
const icons = await renderIconsObject(svgFiles, ICONS_DIR, true);
const iconNodesDirectory = path.resolve(currentDir, '.vitepress/data/lab', 'iconNodes');
if (fs.existsSync(iconNodesDirectory)) {
fs.rmSync(iconNodesDirectory, { recursive: true, force: true });
}
if (!fs.existsSync(iconNodesDirectory)) {
fs.mkdirSync(iconNodesDirectory);
}
const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`);
const iconIndexFileImports: string[] = [];
const iconIndexFileExports: string[] = [];
const iconIndexFileDefaultExports: string[] = [];
const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }]) => {
// We need to use .node.json because the there is a file called package, which is a reserved word for packages.
const location = path.resolve(iconNodesDirectory, `${iconName}.node.json`);
const iconNode = children.map(({ name, attributes }) => [name, attributes]);
const output = JSON.stringify(iconNode, null, 2);
await fs.promises.writeFile(location, output, 'utf-8');
iconIndexFileImports.push(
`import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`,
);
iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`);
iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`);
});
try {
await Promise.all(writeIconFiles);
await fs.promises.writeFile(
iconIndexFile,
`\
${iconIndexFileImports.join('\n')}
export {
${iconIndexFileExports.join('\n')}
}
export default {
${iconIndexFileDefaultExports.join('\n')}
}
`,
'utf-8',
);
console.log('Successfully write', writeIconFiles.length, 'iconNodes.');
} catch (error) {
throw new Error(`Something went wrong generating iconNode files,\n ${error}`);
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import fs from 'fs';
import path from 'path';
import { simpleGit } from 'simple-git';

View File

@@ -1,31 +1,64 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import fs from 'fs';
import path from 'path';
import { loadEnvFile } from 'node:process';
import { readSvgDirectory } from '@lucide/helpers';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../icons');
const iconPopularityDirectory = path.resolve(currentDir, '.vitepress/data', 'iconPopularity');
const dataDirectory = path.resolve(currentDir, '.vitepress/data');
try {
// Load environment variables from .env file, if it exists.
loadEnvFile(`${currentDir}/.env`);
} catch (error) {}
if (fs.existsSync(iconPopularityDirectory)) {
fs.rmSync(iconPopularityDirectory, { recursive: true, force: true });
} catch {
// No .env file, so rely on the environment as-is.
}
if (!fs.existsSync(iconPopularityDirectory)) {
fs.mkdirSync(iconPopularityDirectory);
interface IconCollection {
/** Human readable name, used for logging. */
name: string;
/** Directory containing the icon `.json` files. */
iconsDir: string;
/** Directory the `${iconName}.json` popularity files are written to. */
outputDir: string;
/** Matches an analytics route and captures the icon name in group 1. */
routePattern: RegExp;
}
const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json');
const iconNames = iconJsonFiles.map((file) => path.basename(file, '.json'));
// Lab must come before the default collection because `/icons/lab/foo` also
// matches the default `/icons/(...)` pattern; the first match wins per route.
const iconCollections: IconCollection[] = [
{
name: 'lab',
iconsDir: path.resolve(currentDir, '../lab'),
outputDir: path.resolve(dataDirectory, 'lab', 'iconPopularity'),
// e.g. /icons/lab/burger, /icons/lab/burger/ or /icons/lab/burger?ref=homepage
routePattern: /^\/icons\/lab\/([^\/\?]+)(?:[\/\?]|$)/,
},
{
name: 'default',
iconsDir: path.resolve(currentDir, '../icons'),
outputDir: path.resolve(dataDirectory, 'iconPopularity'),
// e.g. /icons/activity, /icons/activity/ or /icons/activity?ref=homepage
routePattern: /^\/icons\/([^\/\?]+)(?:[\/\?]|$)/,
},
];
const defaultIconNamePopularity: Record<string, number> = Object.fromEntries(
iconNames.map((iconName) => [iconName, 0])
interface CollectionData extends IconCollection {
iconNames: string[];
counts: Record<string, number>;
}
const collections: CollectionData[] = await Promise.all(
iconCollections.map(async (collection) => {
const iconJsonFiles = await readSvgDirectory(collection.iconsDir, '.json');
const iconNames = iconJsonFiles.map((file) => path.basename(file, '.json'));
return {
...collection,
iconNames,
counts: Object.fromEntries(iconNames.map((iconName) => [iconName, 0])),
};
}),
);
interface AnalyticsResponse {
@@ -35,19 +68,30 @@ interface AnalyticsResponse {
}[];
}
const writeIconPopularity = async (iconVisitCounts: Record<string, number>) => {
const writePopularityPromises = Object.entries(iconVisitCounts).map(([iconName, count]) => {
const location = path.resolve(iconPopularityDirectory, `${iconName}.json`);
const writeIconPopularity = async ({ name, outputDir, counts }: CollectionData) => {
if (fs.existsSync(outputDir)) {
fs.rmSync(outputDir, { recursive: true, force: true });
}
fs.mkdirSync(outputDir, { recursive: true });
const writePopularityPromises = Object.entries(counts).map(([iconName, count]) => {
const location = path.resolve(outputDir, `${iconName}.json`);
return fs.promises.writeFile(location, JSON.stringify({ count }, null, 2), 'utf-8');
});
await Promise.all(writePopularityPromises);
console.log('Successfully written icon popularity data for', writePopularityPromises.length, 'icons.');
console.log(
'Successfully written icon popularity data for',
writePopularityPromises.length,
`${name} icons.`,
);
};
const writeAllIconPopularity = () => Promise.all(collections.map(writeIconPopularity));
if (process.env.LUCIDE_ANALYTICS_TOKEN === undefined) {
console.warn('LUCIDE_ANALYTICS_TOKEN environment variable is not set. Writing default icon popularity data.');
await writeIconPopularity(defaultIconNamePopularity);
await writeAllIconPopularity();
process.exit(0);
}
@@ -76,29 +120,34 @@ try {
const analyticsData: AnalyticsResponse = await response.json();
const iconVisitCounts = analyticsData.results.reduce((acc, { dimensions, metrics }) => {
for (const { dimensions, metrics } of analyticsData.results) {
const [route] = dimensions;
const [count] = metrics;
if (!route.startsWith('/icons')) {
return acc
continue;
}
// Only match routes that start with /icons/ and end with an icon name, e.g. /icons/activity, /icons/activity/ or /icons/activity?ref=homepage
const match = route.match(/^\/icons\/([^\/\?]+)(?:[\/\?]|$)/);
if (match) {
const iconName = match[1];
if (iconNames.includes(iconName)) {
acc[iconName] = (acc[iconName] || 0) + count;
// Route each visit to the first collection whose pattern matches. Lab is
// checked first so `/icons/lab/foo` is not swallowed by the default pattern.
for (const collection of collections) {
const match = route.match(collection.routePattern);
if (!match) {
continue;
}
const iconName = match[1];
if (collection.iconNames.includes(iconName)) {
collection.counts[iconName] += count;
}
break;
}
}
return acc;
}, defaultIconNamePopularity);
await writeIconPopularity(iconVisitCounts);
await writeAllIconPopularity();
} catch (error) {
console.error('Error fetching analytics data:', error);
await writeIconPopularity(defaultIconNamePopularity);
await writeAllIconPopularity();
}

View File

@@ -0,0 +1,49 @@
import fs from 'fs';
import path from 'path';
import { readSvgDirectory, toCamelCase } from '@lucide/helpers';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../lab');
const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json');
const labIconPopularityDirectory = path.resolve(currentDir, '.vitepress/data/lab/iconPopularity');
const location = path.resolve(currentDir, labIconPopularityDirectory, 'index.ts');
if (fs.existsSync(location)) {
fs.unlinkSync(location);
}
if (!fs.existsSync(labIconPopularityDirectory)) {
fs.mkdirSync(labIconPopularityDirectory);
}
const iconMetaIndexFileImports = [];
const iconMetaIndexFileExports = [];
iconJsonFiles.forEach((iconJsonFile) => {
const iconName = path.basename(iconJsonFile, '.json');
iconMetaIndexFileImports.push(
`import ${toCamelCase(iconName)}Popularity from './${iconName}.json' with { type: 'json' };`,
);
iconMetaIndexFileExports.push(` '${iconName}': ${toCamelCase(iconName)}Popularity,`);
});
try {
await fs.promises.writeFile(
location,
`\
${iconMetaIndexFileImports.join('\n')}
export default {
${iconMetaIndexFileExports.join('\n')}
}
`,
'utf-8',
);
console.log('Successfully write icon json file index');
} catch (error) {
throw new Error(`Something went wrong generating icon json file index file,\n ${error}`);
}

154
eslint.config.js Normal file
View File

@@ -0,0 +1,154 @@
import path from 'node:path';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import { defineConfig } from 'eslint/config';
import prettier from 'eslint-config-prettier';
import importX, { createNodeResolver } from 'eslint-plugin-import-x';
import htmlEslint from '@html-eslint/eslint-plugin';
import htmlParser from '@html-eslint/parser';
import defaultAttrs from './tools/build-icons/render/default-attrs.json' with { type: 'json' };
import tseslint from 'typescript-eslint';
const gitignorePath = path.join(import.meta.dirname, '.gitignore');
export default defineConfig([
tseslint.configs.recommended,
{
// `packages/angular` has its own config with its own tsconfig, so a single `eslint .` run sees
// two candidate roots and typescript-eslint refuses to guess between them.
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname,
},
},
},
// Everything git ignores (generated icon sources, build output, caches) is ignored here too.
includeIgnoreFile(gitignorePath),
{
// Ignores that are not in .gitignore, because these files are committed.
ignores: [
'lib',
'**/tests',
'packages/**/tests/*',
'docs/images',
'docs/**/examples/',
'docs/.vitepress/theme/components/editors/preact/index.js',
'packages/svelte/.svelte-kit',
// Tracked in git despite matching a .gitignore pattern, so lint it.
'!packages/lucide-react/dynamicIconImports.mjs',
],
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
global: 'readonly',
__dirname: 'readonly',
},
},
plugins: {
'import-x': importX,
},
settings: {
'import-x/resolver-next': [createNodeResolver()],
},
rules: {
...js.configs.recommended.rules,
...prettier.rules,
'no-console': 'off',
'no-param-reassign': 'off',
'no-shadow': 'off',
'no-use-before-define': 'off',
'import-x/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/*.test.js',
'**/*.spec.js',
'**/scripts/**',
'eslint.config.js',
'packages/**/tests/**',
],
},
],
'import-x/extensions': [
'error',
{
pattern: {
mjs: 'always',
json: 'always',
},
},
],
},
},
{
rules: {
// Omitting a property by destructuring it away (`const { key, ...attrs } = node`) leaves the
// omitted binding unused on purpose, so don't report it.
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
},
},
{
files: ['./icons/*.svg'],
languageOptions: {
parser: htmlParser,
},
plugins: {
'@html-eslint': htmlEslint,
},
rules: {
'@html-eslint/require-doctype': 'off',
'@html-eslint/no-duplicate-attrs': 'error',
'@html-eslint/no-inline-styles': 'error',
'@html-eslint/require-attrs': [
'error',
...Object.entries(defaultAttrs).map(([attr, value]) => ({
tag: 'svg',
attr,
value: String(value),
})),
],
'@html-eslint/indent': ['error', 2],
'@html-eslint/no-multiple-empty-lines': ['error', { max: 0 }],
'@html-eslint/no-extra-spacing-attrs': [
'error',
{
enforceBeforeSelfClose: true,
},
],
'@html-eslint/attrs-newline': [
'error',
{
inline: ['path', 'line', 'polyline', 'polygon', 'rect', 'circle', 'ellipse'],
},
],
'@html-eslint/require-closing-tags': [
'error',
{
selfClosing: 'always',
// Inside <svg> every tag counts as "foreign", where the rule only checks that a tag
// already written `/>` stays that way. Listing the shapes here is what actually forces
// `<path ...></path>` to become `<path ... />`.
selfClosingCustomPatterns: ['^(path|line|polyline|polygon|rect|circle|ellipse)$'],
},
],
'@html-eslint/no-restricted-attr-values': [
'error',
{
attrPatterns: ['^(fill|stroke)$'],
attrValuePatterns: ['^(?!(none|currentColor)$).*$'],
message:
'Icons must inherit their colors: `fill` and `stroke` may only be `none` or `currentColor`.',
},
],
'@html-eslint/element-newline': 'error',
'@html-eslint/no-trailing-spaces': 'error',
'@html-eslint/quotes': 'error',
},
},
]);

View File

@@ -40,7 +40,7 @@
}
},
"dependentRequired": {
"deprecated": ["deprecationReason", "toBeRemovedInVersion"],
"deprecated": ["deprecationReason"],
"deprecationReason": ["deprecated"],
"toBeRemovedInVersion": ["deprecated"]
}
@@ -134,7 +134,7 @@
}
},
"dependentRequired": {
"deprecated": ["deprecationReason", "toBeRemovedInVersion"],
"deprecated": ["deprecationReason"],
"deprecationReason": ["deprecated"],
"toBeRemovedInVersion": ["deprecated"]
},

View File

@@ -13,4 +13,4 @@
<path d="M15.697 14h5.606" />
<path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" />
<path d="M3.304 13h6.392" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -21,10 +21,15 @@
"paid",
"partner",
"promo",
"sponsor"
"sponsor",
"audio description",
"video description",
"described video",
"visual description"
],
"categories": [
"multimedia",
"accessibility",
"notifications"
]
}

View File

@@ -23,8 +23,7 @@
{
"name": "alarm-check",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}

View File

@@ -19,8 +19,7 @@
{
"name": "alarm-minus",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}

View File

@@ -19,8 +19,7 @@
{
"name": "alarm-plus",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}

View File

@@ -11,10 +11,9 @@
>
<path d="M10 10H6" />
<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2" />
<path
d="M19 18h2a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.684-.948l-1.923-.641a1 1 0 0 1-.578-.502l-1.539-3.076A1 1 0 0 0 16.382 8H14" />
<path d="M19 18h2a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.684-.948l-1.923-.641a1 1 0 0 1-.578-.502l-1.539-3.076A1 1 0 0 0 16.382 8H14" />
<path d="M8 8v4" />
<path d="M9 18h6" />
<circle cx="17" cy="18" r="2" />
<circle cx="7" cy="18" r="2" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 550 B

34
icons/angle.json Normal file
View File

@@ -0,0 +1,34 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"samuelalake"
],
"use-cases": [
"rotation input in a design inspector",
"measurement tool in cad software",
"dimensioning in technical drawings",
"labeling angles in geometry exercises",
"displaying degrees for rotation controls"
],
"tags": [
"angle",
"degree",
"corner",
"geometry",
"measure",
"math",
"rotate",
"vertex",
"slope",
"protractor",
"measurement",
"dimension",
"arc",
"drafting",
"cad"
],
"categories": [
"math",
"design"
]
}

14
icons/angle.svg Normal file
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="M3 3v16a2 2 0 0 0 2 2h16" />
<path d="M3 11a10 10 0 0 1 10 10" />
</svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -25,8 +25,8 @@
{
"name": "arrow-down-01",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-down-01"
]
}

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