diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 206b4c999..f2517f0a6 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -8,9 +8,5 @@ squircle strikethrough touchpad ungroup -pilcrow - -# Brands -codepen -codesandbox -dribbble +toc +sunlounger diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index ffae92223..000000000 --- a/.eslintignore +++ /dev/null @@ -1,10 +0,0 @@ -dist -build -coverage -lib -tests -node_modules -.eslintrc.js -docs/images -docs/**/examples/ -packages/lucide-react/dynamicIconImports.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index ae2e17576..000000000 --- a/.eslintrc.js +++ /dev/null @@ -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', - }, - }, - ], -}; diff --git a/.github/DISCUSSION_TEMPLATE/icon-design.yml b/.github/DISCUSSION_TEMPLATE/icon-design.yml new file mode 100644 index 000000000..2b753e3b9 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/icon-design.yml @@ -0,0 +1,49 @@ +title: 'Icon Request: ' +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 diff --git a/.github/ISSUE_TEMPLATE/01_icon_request.yml b/.github/ISSUE_TEMPLATE/01_icon_request.yml index a3c4bfca1..a7f2daa73 100644 --- a/.github/ISSUE_TEMPLATE/01_icon_request.yml +++ b/.github/ISSUE_TEMPLATE/01_icon_request.yml @@ -7,8 +7,10 @@ body: value: | Before submitting an icon request check if it has already been requested. If there is an open request, please add a 👍. - **Important note**: No new brand logos are allowed, see https://github.com/lucide-icons/lucide/issues/670. - Existing brand icons will also be phased out. For brand icons, consider using https://simpleicons.org, which offers purpose-built SVGs that are also on a 24×24px grid. + > [!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 are being 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: @@ -41,9 +43,9 @@ body: 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. + - 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, political imagery or hate symbols. + - 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 diff --git a/.github/ISSUE_TEMPLATE/02_bug_report.yml b/.github/ISSUE_TEMPLATE/02_bug_report.yml index 21dd4e1f2..f5d8918bb 100644 --- a/.github/ISSUE_TEMPLATE/02_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/02_bug_report.yml @@ -13,15 +13,19 @@ body: description: Which Lucide packages are affected? You may select more than one. options: - label: lucide - - label: lucide-angular - - label: lucide-flutter - - label: lucide-preact - label: lucide-react + - label: '@lucide/vue' + - label: '@lucide/svelte' + - label: '@lucide/angular' + - label: '@lucide/astro' - label: lucide-react-native + - label: lucide-preact - label: lucide-solid - - label: lucide-svelte - - label: lucide-vue - - label: lucide-vue-next + - label: lucide-static + - label: '@lucide/icons' + - label: lucide-vue-next (deprecated) + - label: lucide-svelte (deprecated) + - label: lucide-angular (deprecated) - label: Figma plugin - label: source/main - label: other/not relevant @@ -32,7 +36,7 @@ body: attributes: label: Version description: What version of Lucide are you running? - placeholder: e.g. 0.289.1 + placeholder: e.g. 1.1.0 validations: required: true - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/04_feature_request.yml b/.github/ISSUE_TEMPLATE/04_feature_request.yml index 9b07f3645..73d354ac8 100644 --- a/.github/ISSUE_TEMPLATE/04_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/04_feature_request.yml @@ -13,18 +13,23 @@ body: description: Which Lucide project do you wish this feature were added to? You may select more than one. options: - label: lucide - - label: lucide-angular - - label: lucide-flutter - - label: lucide-preact - label: lucide-react + - label: '@lucide/vue' + - label: '@lucide/svelte' + - label: '@lucide/angular' + - label: '@lucide/astro' - label: lucide-react-native + - label: lucide-preact - label: lucide-solid - - label: lucide-svelte - - label: lucide-vue - - label: lucide-vue-next + - label: lucide-static + - label: '@lucide/icons' + - label: lucide-vue-next (deprecated) + - label: lucide-svelte (deprecated) + - label: lucide-angular (deprecated) - label: Figma plugin - label: all JS packages - label: site + - label: other/not relevant validations: required: true - type: textarea diff --git a/.github/actions/build-and-test.yml b/.github/actions/build-and-test.yml deleted file mode 100644 index 5b5a4675f..000000000 --- a/.github/actions/build-and-test.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: 'Build and Test' -description: 'Builds and test a package' - -inputs: - name: - description: “Name of the package” - required: true - -runs: - using: 'composite' - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 16 - - - uses: pnpm/action-setup@v2 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - name: Setup pnpm cache - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install --filter lucide-preact - - - name: Build - run: pnpm --filter lucide-preact build - - - name: Test - run: pnpm --filter lucide-preact test diff --git a/.github/actions/check-icons.yml b/.github/actions/check-icons.yml deleted file mode 100644 index 0850a17ac..000000000 --- a/.github/actions/check-icons.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: 'Check icons' -description: 'Cross-checks icon and category references in JSON descriptors' - -inputs: - name: - description: “Name of the package” - required: true - -runs: - using: 'composite' - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 16 - - - uses: pnpm/action-setup@v2 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - name: Setup pnpm cache - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install --filter . - - - name: Check icons and categories - run: pnpm checkIcons diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..055190392 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,8 @@ +## Reviewing PRs + +When reviewing PRs, please respond in a friendly and constructive manner. + +- First, thank the contributor for their contribution in an enthusiastic way. +- If you have any suggestions for improvement, provide them clearly and concisely. +- If you have any questions about the PR, ask them respectfully. +- Let the contributor know that we have many PRs to review and that we appreciate their patience while we review their contribution. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..a26b1f2f5 --- /dev/null +++ b/.github/dependabot.yml @@ -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: + - '*' diff --git a/.github/instructions/metadata.instructions.md b/.github/instructions/metadata.instructions.md new file mode 100644 index 000000000..05aacbae1 --- /dev/null +++ b/.github/instructions/metadata.instructions.md @@ -0,0 +1,20 @@ +--- +applyTo: "icons/*.json" +--- +# JSON Metadata Descriptor + +The schema for the JSON metadata descriptor can be found in `icon.schema.json`. It defines the required and optional properties for the JSON files that accompany each icon. The JSON metadata descriptor should be placed in the same directory as the SVG file of the icon and should have the same name as the SVG file, but with a `.json` extension. For example, if the SVG file is named `home.svg`, the JSON metadata descriptor should be named `home.json`. + +## Contributors + +The `contributors` property is a required array of GitHub usernames for the people who created or contributed to the icon. It is used to give credit to contributors and to track the icon's history. Add the PR author to the `contributors` array if they are not already listed and they have made a significant contribution to the icon's SVG. + +## Tags + +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 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 + +The `categories` property is an array of strings that specify the categories to which the icon belongs, such as "devices", "interface", "media", etc. See the `categories` property in the `icon.schema.json` for more details on the allowed values. The categories should be chosen based on the use cases provided in the PR description and the existing categories in the repository. The categories should be relevant to the icon and should help users find the icon when searching for specific types of icons. The name of icon should not be included in the categories, as it is already specified. Suggest categories based on the name of the icon and the use cases provided in the PR description. Use the existing categories in the repository as a reference for consistency and to avoid duplicates. Categories should always be in lowercase and should not contain spaces. diff --git a/.github/labeler.yml b/.github/labeler.yml index 2023caf3d..aeb274b50 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -36,57 +36,62 @@ - any-glob-to-any-file: - 'packages/lucide/*' -# For changes in the lucide React package +# For changes in the Lucide React package ⚛️ react package: - changed-files: - any-glob-to-any-file: - 'packages/lucide-react/*' -# For changes in the lucide React Native package +# For changes in the Lucide React Native package ⚛️ react native package: - changed-files: - any-glob-to-any-file: - 'packages/lucide-react-native/*' -# For changes in the lucide vue packages +# For changes in the Lucide vue packages 💎 vue package: - changed-files: - any-glob-to-any-file: - - 'packages/lucide-vue/*' - - 'packages/lucide-vue-next/*' + - 'packages/vue/*' -# For changes in the lucide angular package +# For changes in the Lucide angular package 🅰️ angular package: - changed-files: - any-glob-to-any-file: - - 'packages/lucide-angular/*' + - 'packages/angular/*' -# For changes in the lucide preact package +# For changes in the Lucide preact package ⚛️ preact package: - changed-files: - any-glob-to-any-file: - 'packages/lucide-preact/*' -# For changes in the lucide svelte package +# For changes in the Lucide svelte package 🧣 svelte package: - changed-files: - any-glob-to-any-file: - - 'packages/lucide-svelte/*' + - 'packages/svelte/*' -# For changes in the lucide solid package +# For changes in the Lucide solid package 🪝 solid package: - changed-files: - any-glob-to-any-file: - 'packages/lucide-solid/*' -# For changes in the lucide static package +# For changes in the Lucide astro package +🚀 astro package: + - changed-files: + - any-glob-to-any-file: + - 'packages/astro/*' + +# For changes in the @lucide/icons package +❇️ lucide-icons: + - changed-files: + - any-glob-to-any-file: + - 'packages/lucide-icons/*' + +# For changes in the Lucide static package 🪨 static package: - changed-files: - any-glob-to-any-file: - 'packages/lucide-static/*' - -# For changes in the lucide flutter package -🏹 flutter package: - - changed-files: - - any-glob-to-any-file: - - 'packages/lucide-flutter/*' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2403cee42..3bf6bfff1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,18 @@ + + - -## What is the purpose of this pull request? - -- [ ] New Icon -- [ ] Bug fix -- [ ] New Feature -- [ ] Documentation update -- [ ] Other: - -### Description +## Description ### Icon use case @@ -23,10 +25,12 @@ ### Concept + + - [ ] I have provided valid use cases for each icon. -- [ ] I have not added any a brand or logo icon. +- [ ] I have [not added any brand or logo icon](https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md). - [ ] I have not used any hate symbols. -- [ ] I have not included any religious or political imagery. +- [ ] I have not included any religious, war/violence related or political imagery. ### Author, credits & license @@ -37,13 +41,15 @@ ### Naming -- [ ] I've read and followed the [naming conventions](https://lucide.dev/guide/design/icon-design-guide#naming-conventions) +- [ ] I've read and followed the [naming conventions](https://lucide.dev/contribute/icon-design-guide#naming-conventions) - [ ] I've named icons by what they are rather than their use case. - [ ] I've provided meta JSON files in `icons/[iconName].json`. ### Design -- [ ] I've read and followed the [icon design guidelines](https://lucide.dev/guide/design/icon-design-guide) + +- [ ] 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. diff --git a/.github/workflows/angular.yml b/.github/workflows/angular.yml new file mode 100644 index 000000000..764d077ec --- /dev/null +++ b/.github/workflows/angular.yml @@ -0,0 +1,44 @@ +name: Lucide Angular checks + +on: + pull_request: + paths: + - packages/angular/** + - tools/build-icons/** + - 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/angular build + + test: + 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: Test + run: pnpm --filter @lucide/angular test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b6f6fed5..c410bd739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,22 +7,70 @@ on: paths: - icons/**/*.svg + workflow_dispatch: + inputs: + version: + description: Version + required: true + tag: + required: true + description: NPM tag + default: 'latest' + type: choice + options: + - latest + - next + - beta + - alpha + +permissions: {} + jobs: + check-dispatch-gate: + if: github.repository == 'lucide-icons/lucide' && + 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Fetch tags + run: git fetch --all --tags + + - name: Check if latest tag was created today + id: environment + run: | + LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) + TAG_DATE=$(git log -1 --format=%cs "$LATEST_TAG") + TODAY=$(date +%Y-%m-%d) + + if [ "$TAG_DATE" == "$TODAY" ]; then + echo "::warning::A release tag already exists for today ($TODAY): $LATEST_TAG. Manual approval will be required to proceed." + echo "RELEASE_ENVIRONMENT=Icon Release Gated" >> $GITHUB_OUTPUT + else + echo "RELEASE_ENVIRONMENT=Icon Release" >> $GITHUB_OUTPUT + fi + create-release: if: github.repository == 'lucide-icons/lucide' + 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@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -32,27 +80,21 @@ jobs: - name: Get latest tag id: latest-tag - run: echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT + run: echo "LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)" >> $GITHUB_OUTPUT + + - name: Log latest tag + run: echo '${{ steps.latest-tag.outputs.LATEST_TAG }}' - name: Check if we can patch - run: .github/workflows/version-up.sh --minor + run: pnpm semver $LATEST_TAG -i minor + env: + LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} - name: Create new version id: new-version - run: echo "NEW_VERSION=$(.github/workflows/version-up.sh --minor)" >> $GITHUB_OUTPUT - - - name: Create change log - id: change-log - run: | - CHANGE_LOG=$(pnpm run generate:changelog --old-tag=${{ steps.latest-tag.outputs.LATEST_TAG }}) - CHANGE_LOG=$(tail -n +5 <<< $CHANGE_LOG) - echo $CHANGE_LOG - EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) - echo "CHANGE_LOG<<$EOF" >> $GITHUB_OUTPUT - echo "$CHANGE_LOG" >> $GITHUB_OUTPUT - echo "$EOF" >> $GITHUB_OUTPUT + run: echo "NEW_VERSION=$(pnpm semver $LATEST_TAG -i minor)" >> $GITHUB_OUTPUT env: - GITHUB_API_KEY: ${{ secrets.GITHUB_TOKEN }} + LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} - name: Check output run: | @@ -60,16 +102,30 @@ 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: New icons ${{ steps.new-version.outputs.NEW_VERSION }} - body: ${{ steps.change-log.outputs.CHANGE_LOG }} + name: Version ${{ steps.new-version.outputs.NEW_VERSION }} + generate_release_notes: true start-release: if: github.repository == 'lucide-icons/lucide' 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.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 }} diff --git a/.github/workflows/close-issue-with-banned-phrases.yml b/.github/workflows/close-issue-with-banned-phrases.yml new file mode 100644 index 000000000..de14b3000 --- /dev/null +++ b/.github/workflows/close-issue-with-banned-phrases.yml @@ -0,0 +1,110 @@ +name: Close Icon Requests with Brand Terms + +on: + issues: + types: [opened] + discussion: + types: [created] + +permissions: {} + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + block_phrases: + if: github.repository == 'lucide-icons/lucide' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + discussions: write + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Load stopwords from JSON & check issue title & body + if: github.event_name == 'issues' && contains(github.event.issue.labels.*.name, '🙌 icon request') + run: | + ISSUE_TITLE=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH") + ISSUE_BODY=$(jq -r '.issue.body // ""' "$GITHUB_EVENT_PATH") + ICON_NAME_SECTION=$(printf '%s\n' "$ISSUE_BODY" | awk '/### Icon name/{flag=1; next} /^### /{flag=0} flag') + + jq -r 'to_entries[] | "\(.key) \(.value)"' brand-stopwords.json | while read -r KEY VALUE; do + SAFE_KEY=$(printf '%s\n' "$KEY" | sed 's/[][\.^$*]/\\&/g') + SAFE_VALUE=$(printf '%s\n' "$VALUE" | sed 's/[][\.^$*]/\\&/g') + + if echo "$ISSUE_TITLE" | grep -iqE "$SAFE_KEY|$SAFE_VALUE" || \ + { [ -n "$ICON_NAME_SECTION" ] && echo "$ICON_NAME_SECTION" | grep -iqE "$SAFE_KEY|$SAFE_VALUE"; }; then + + URL_ENCODED_VALUE=$(jq -rn --arg val "$VALUE" '$val|@uri') + + gh issue close ${{ github.event.issue.number }} \ + --reason "not planned" \ + --comment "It looks like this request is about **${VALUE}**, which is a brand logo. + + 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.](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+${URL_ENCODED_VALUE}) + + We’re always happy to help on [Discord](https://discord.gg/EH6nSts)." + + gh issue lock ${{ github.event.issue.number }} --reason spam + exit 0 + fi + done + + - name: Load stopwords from JSON & check discussion title & body + if: github.event_name == 'discussion' && github.event.discussion.category.slug == 'icon-design' + run: | + DISCUSSION_TITLE=$(jq -r '.discussion.title' "$GITHUB_EVENT_PATH") + DISCUSSION_BODY=$(jq -r '.discussion.body // ""' "$GITHUB_EVENT_PATH") + ICON_NAME_SECTION=$(printf '%s\n' "$DISCUSSION_BODY" | awk '/### Icon name/{flag=1; next} /^### /{flag=0} flag') + + jq -r 'to_entries[] | "\(.key) \(.value)"' brand-stopwords.json | while read -r KEY VALUE; do + SAFE_KEY=$(printf '%s\n' "$KEY" | sed 's/[][\.^$*]/\\&/g') + SAFE_VALUE=$(printf '%s\n' "$VALUE" | sed 's/[][\.^$*]/\\&/g') + + if echo "$DISCUSSION_TITLE" | grep -iqE "$SAFE_KEY|$SAFE_VALUE" || \ + { [ -n "$ICON_NAME_SECTION" ] && echo "$ICON_NAME_SECTION" | grep -iqE "$SAFE_KEY|$SAFE_VALUE"; }; then + + URL_ENCODED_VALUE=$(jq -rn --arg val "$VALUE" '$val|@uri') + DISCUSSION_NODE_ID=$(jq -r '.discussion.node_id' "$GITHUB_EVENT_PATH") + COMMENT_BODY="It looks like this request is about **${VALUE}**, which is a brand logo. + + 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.](https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md) + + You can [search for similar discussions.](https://github.com/lucide-icons/lucide/discussions/categories/icon-design?discussions_q=category%253A%22Icon+design%22+${URL_ENCODED_VALUE}) + + We’re always happy to help on [Discord](https://discord.gg/EH6nSts)." + + # Add comment to discussion + gh api graphql -f query=' + mutation($discussionId: ID!, $body: String!) { + addDiscussionComment(input: {discussionId: $discussionId, body: $body}) { + comment { id } + } + }' -f discussionId="$DISCUSSION_NODE_ID" -f body="$COMMENT_BODY" + + # Close discussion + gh api graphql -f query=' + mutation($discussionId: ID!) { + closeDiscussion(input: {discussionId: $discussionId}) { + discussion { closed } + } + }' -f discussionId="$DISCUSSION_NODE_ID" + + # Lock discussion + gh api graphql -f query=' + mutation($lockableId: ID!, $reason: LockReason) { + lockLockable(input: {lockableId: $lockableId, lockReason: $reason}) { + lockedRecord { locked } + } + }' -f lockableId="$DISCUSSION_NODE_ID" -f reason="SPAM" + exit 0 + fi + done diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml index 0b47315f4..7704f341c 100644 --- a/.github/workflows/close-stale-prs.yml +++ b/.github/workflows/close-stale-prs.yml @@ -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. diff --git a/.github/workflows/comment-icon-preview.yml b/.github/workflows/comment-icon-preview.yml new file mode 100644 index 000000000..b9f813e64 --- /dev/null +++ b/.github/workflows/comment-icon-preview.yml @@ -0,0 +1,65 @@ +name: Icon preview comment + +on: + workflow_run: + workflows: ['Pull request icon preview'] + 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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "pr_number" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/pr_number.zip', Buffer.from(download.data)); + + - name: 'Unzip artifact' + run: unzip pr_number.zip + + - name: 'Get PR number' + run: echo "number=$(cat NR)" >> $GITHUB_OUTPUT + id: pr-number + + - name: Find Comment + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 + id: pr-comment + with: + issue-number: ${{ steps.pr-number.outputs.number }} + comment-author: 'github-actions[bot]' + body-includes: Added or changed icons + + - name: Create or update comment + 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 }} + body-path: comment-markup.md + edit-mode: replace diff --git a/.github/workflows/icons.yml b/.github/workflows/icons.yml new file mode 100644 index 000000000..e4d53a6f9 --- /dev/null +++ b/.github/workflows/icons.yml @@ -0,0 +1,46 @@ +name: Lucide Icons Checks + +on: + pull_request: + paths: + - packages/icons/** + - 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/icons build + + test: + 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: Test + run: pnpm --filter @lucide/icons test diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index bf474c478..75de3b3bb 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -2,11 +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/labeler@v5 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml new file mode 100644 index 000000000..edaec6611 --- /dev/null +++ b/.github/workflows/lint-code.yml @@ -0,0 +1,24 @@ +name: Linting PR code + +on: + pull_request: + +permissions: + contents: read + +jobs: + lint-code: + 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: Run Linter + run: pnpm lint diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml new file mode 100644 index 000000000..ef9abcf61 --- /dev/null +++ b/.github/workflows/lint-pr-title.yml @@ -0,0 +1,36 @@ +name: Linting PR title + +on: + pull_request: + types: + - opened + - edited + - reopened + +permissions: + contents: read + pull-requests: read # Required to read the PR title + +jobs: + lint-pr-title: + name: PR Title Lint + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + fix + feat + perf + refactor + test + style + docs + ci + build + chore + requireScope: true + ignoreLabels: | + bot diff --git a/.github/workflows/linting-icons.yml b/.github/workflows/linting-icons.yml new file mode 100644 index 000000000..0f69ba490 --- /dev/null +++ b/.github/workflows/linting-icons.yml @@ -0,0 +1,72 @@ +name: Linting Icons + +on: + push: + branches: + - next + pull_request: + paths: + - 'icons/*' + - 'lab/*' + +permissions: {} + +jobs: + lint-filenames: + name: Lint Filenames + if: github.repository == 'lucide-icons/lucide' + 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: Get changed files + id: changed-files + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + 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@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 '" diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml deleted file mode 100644 index cdc76ea5a..000000000 --- a/.github/workflows/linting.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Linting - -on: - pull_request: - branches: - - '**' - -jobs: - linting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install Dependencies - run: pnpm install --frozen-lockfile - - - name: Run Linter - run: pnpm lint diff --git a/.github/workflows/lucide-angular.yml b/.github/workflows/lucide-angular.yml deleted file mode 100644 index 46c9a6e58..000000000 --- a/.github/workflows/lucide-angular.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Lucide Angular checks - -on: - pull_request: - paths: - - packages/lucide-angular/** - - tools/build-icons/** - - pnpm-lock.yaml - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm --filter lucide-angular build - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Test - run: pnpm --filter lucide-angular test diff --git a/.github/workflows/lucide-astro.yml b/.github/workflows/lucide-astro.yml new file mode 100644 index 000000000..47a1fc8dd --- /dev/null +++ b/.github/workflows/lucide-astro.yml @@ -0,0 +1,48 @@ +name: Lucide Astro Checks + +on: + push: + branches: + - next + pull_request: + paths: + - packages/astro/** + - packages/shared/** + - tools/build-icons/** + - 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: + node-version-file: 'package.json' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm --filter @lucide/astro build + + test: + 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: + node-version-file: 'package.json' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test + run: pnpm --filter @lucide/astro test diff --git a/.github/workflows/lucide-font.yml b/.github/workflows/lucide-font.yml index 5f183f813..7548723cb 100644 --- a/.github/workflows/lucide-font.yml +++ b/.github/workflows/lucide-font.yml @@ -1,36 +1,39 @@ name: Lucide font checks on: + push: + branches: + - next pull_request: paths: - - icons/** - tools/build-font/** - pnpm-lock.yaml +permissions: + contents: read + jobs: lucide-font: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Outline svg Icons - run: pnpm build:outline-icons + - name: Execute unit tests for build-font + run: pnpm test:font - name: Create font in ./lucide-font run: pnpm build:font - name: 'Upload to Artifacts' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: lucide-font path: lucide-font diff --git a/.github/workflows/lucide-lab.yml b/.github/workflows/lucide-lab.yml new file mode 100644 index 000000000..323cb3595 --- /dev/null +++ b/.github/workflows/lucide-lab.yml @@ -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 diff --git a/.github/workflows/lucide-preact.yml b/.github/workflows/lucide-preact.yml index c6c598d7a..f842c1b47 100644 --- a/.github/workflows/lucide-preact.yml +++ b/.github/workflows/lucide-preact.yml @@ -1,25 +1,30 @@ name: Lucide Preact Checks on: + push: + branches: + - next pull_request: paths: - packages/lucide-preact/** + - packages/shared/** - tools/build-icons/** - tools/rollup-plugins/** - pnpm-lock.yaml +permissions: + contents: read + jobs: lucide-preact: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/lucide-react-native.yml b/.github/workflows/lucide-react-native.yml index c68341303..6f8d83079 100644 --- a/.github/workflows/lucide-react-native.yml +++ b/.github/workflows/lucide-react-native.yml @@ -1,25 +1,46 @@ name: Lucide React Native checks on: + push: + branches: + - next pull_request: paths: - packages/lucide-react-native/** + - packages/shared/** - tools/build-icons/** - tools/rollup-plugins/** - pnpm-lock.yaml +permissions: + contents: read + jobs: - lucide-react-native: + build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm --filter lucide-react-native build + + test: + 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 diff --git a/.github/workflows/lucide-react.yml b/.github/workflows/lucide-react.yml index b5b2b92e2..7d00a1ba5 100644 --- a/.github/workflows/lucide-react.yml +++ b/.github/workflows/lucide-react.yml @@ -1,26 +1,31 @@ name: Lucide React Checks on: + push: + branches: + - next pull_request: paths: - packages/lucide-react/** + - packages/shared/** - tools/build-icons/** - tools/rollup-plugins/** - scripts/generateNextJSAliases.mjs - pnpm-lock.yaml +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -31,14 +36,12 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/lucide-shared.yml b/.github/workflows/lucide-shared.yml new file mode 100644 index 000000000..efea0dc77 --- /dev/null +++ b/.github/workflows/lucide-shared.yml @@ -0,0 +1,30 @@ +name: Lucide Shared Checks + +on: + push: + branches: + - next + pull_request: + paths: + - packages/shared/** + - pnpm-lock.yaml + +permissions: + contents: read + +jobs: + test: + 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: Test + run: pnpm --filter lucide-react test diff --git a/.github/workflows/lucide-solid.yml b/.github/workflows/lucide-solid.yml index 6f67075f3..d78602a35 100644 --- a/.github/workflows/lucide-solid.yml +++ b/.github/workflows/lucide-solid.yml @@ -1,25 +1,30 @@ name: Lucide Solid Checks on: + push: + branches: + - next pull_request: paths: - packages/lucide-solid/** + - 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@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -30,14 +35,12 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/lucide-static.yml b/.github/workflows/lucide-static.yml index fc0fc4b99..fee24bf72 100644 --- a/.github/workflows/lucide-static.yml +++ b/.github/workflows/lucide-static.yml @@ -7,18 +7,19 @@ on: - tools/build-icons/** - pnpm-lock.yaml +permissions: + contents: read + jobs: lucide-static: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/lucide-svelte-5.yml b/.github/workflows/lucide-svelte-5.yml new file mode 100644 index 000000000..4f48fcf19 --- /dev/null +++ b/.github/workflows/lucide-svelte-5.yml @@ -0,0 +1,65 @@ +name: Lucide Svelte 5 checks + +on: + push: + branches: + - next + pull_request: + paths: + - packages/svelte/** + - 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/svelte build + + test: + 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: Test + run: pnpm --filter @lucide/svelte test + + typecheck: + 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: Test + run: pnpm --filter @lucide/svelte typecheck diff --git a/.github/workflows/lucide-svelte.yml b/.github/workflows/lucide-svelte.yml deleted file mode 100644 index 4fa768db2..000000000 --- a/.github/workflows/lucide-svelte.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Lucide Svelte checks - -on: - pull_request: - paths: - - packages/lucide-svelte/** - - tools/build-icons/** - - tools/rollup-plugins/** - - pnpm-lock.yaml - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm --filter lucide-svelte build - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Test - run: pnpm --filter lucide-svelte test diff --git a/.github/workflows/lucide-vue-next.yml b/.github/workflows/lucide-vue-next.yml deleted file mode 100644 index 07310b8e9..000000000 --- a/.github/workflows/lucide-vue-next.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Lucide Vue Next checks - -on: - pull_request: - paths: - - packages/lucide-vue-next/** - - tools/build-icons/** - - tools/rollup-plugins/** - - pnpm-lock.yaml - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm --filter lucide-vue-next build - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Test - run: pnpm --filter lucide-vue-next test diff --git a/.github/workflows/lucide-vue.yml b/.github/workflows/lucide-vue.yml index c409956dc..e4e4ee9b6 100644 --- a/.github/workflows/lucide-vue.yml +++ b/.github/workflows/lucide-vue.yml @@ -1,46 +1,49 @@ name: Lucide Vue checks on: + push: + branches: + - 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@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build - run: pnpm --filter lucide-vue build + run: pnpm --filter @lucide/vue build test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Test - run: pnpm --filter lucide-vue test + run: pnpm --filter @lucide/vue test diff --git a/.github/workflows/lucide.yml b/.github/workflows/lucide.yml index 9f8083cdf..844d2647f 100644 --- a/.github/workflows/lucide.yml +++ b/.github/workflows/lucide.yml @@ -1,6 +1,9 @@ name: Lucide checks on: + push: + branches: + - next pull_request: paths: - packages/lucide/** @@ -8,18 +11,19 @@ on: - tools/rollup-plugins/** - pnpm-lock.yaml +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -30,14 +34,12 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v3.8.1 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/pull-request-icon-preview.yml b/.github/workflows/pull-request-icon-preview.yml new file mode 100644 index 000000000..62112e892 --- /dev/null +++ b/.github/workflows/pull-request-icon-preview.yml @@ -0,0 +1,49 @@ +name: Pull request icon preview + +on: + pull_request: + 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@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@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: icons/*.svg + + - 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 + + - name: Save PR number + run: | + mkdir -p ./pr + echo ${{ github.event.number }} > ./pr/NR + + - name: Generate comment markup + run: node ./scripts/generateChangedIconsCommentMarkup.mts >> ./pr/comment-markup.md + id: comment-markup + env: + CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr_number + path: pr/ diff --git a/.github/workflows/pull-request-metadata-suggestions.yml b/.github/workflows/pull-request-metadata-suggestions.yml new file mode 100644 index 000000000..cc1586da7 --- /dev/null +++ b/.github/workflows/pull-request-metadata-suggestions.yml @@ -0,0 +1,40 @@ +name: Pull request metadata suggestions + +on: + pull_request_target: + 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 + pull-requests: write + + 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: lucide-icons/lucide + - 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: Generate comment markup + run: node ./scripts/suggestMetaData.mts + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + PULL_REQUEST_NUMBER: ${{ github.event.number }} + COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml deleted file mode 100644 index a20441278..000000000 --- a/.github/workflows/pull-request.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Add Changed Icons comment - -on: - pull_request_target: - paths: - - 'icons/*' - branches: - - main - - fix-icon-preview - -jobs: - lint-filenames: - if: github.repository == 'lucide-icons/lucide' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - 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 - with: - files: icons/* - - name: Generate annotations - run: node ./scripts/lintFilenames.mjs - env: - CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - - lint-contributors: - if: github.repository == 'lucide-icons/lucide' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - 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 - with: - files: icons/* - - uses: actions/setup-node@v4 - - name: Install simple-git (safer and faster than installing all deps) - run: npm install simple-git - - name: Generate annotations - run: node ./scripts/updateContributors.mjs - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FETCH_DEPTH: ${{ github.event.pull_request.commits }} - CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - - name: Generate annotations - env: - ANNOTATION_SEVERITY: notice - ANNOTATION_TITLE: Contributors have changed! - ANNOTATION_DESCRIPTION: Don't add people who have only performed automatic optimizations. - run: | - git diff --unified=0 -- icons/*.json | # diff icon metadata (unified=0 gives the correct chunk line number) - perl -ne '/^(\+|- |@)/ && print' | # get chunks (lines that start with "+++", "@@", "+ ", "- ") - perl -pe 's/\n/%0A/' | # url encode line breaks (\n -> %0A) - perl -pe 's/%0A(\+\+\+ b\/)/\n\1/g' | # split chunks(one chunk per line) - perl -pe "s/\+\+\+ b\/([^@]*)%0A@@ -(\d+)[^\s]* \+(\d+)[^@]*@@(.*)/::$ANNOTATION_SEVERITY file=\1,line=\2,endLine=\3,title=$ANNOTATION_TITLE::$ANNOTATION_DESCRIPTION%0A\4/" - # Example for the previous substitution - # input: +++ b/icons/accessibility.json%0A@@ -2,0 +3 @@%0A+ "contributors": ["hi"],%0A@@ -13 +14 @@%0A+}%0A - # output: ::$ANNOTATION_SEVERITY file=icons/accessibility.json,line=2,endLine=3,title=$ANNOTATION_TITLE::$ANNOTATION_DESCRIPTION%0A%0A+ "contributors": ["hi"],%0A@@ -13 +14 @@%0A+}%0A - - lint-aliases: - name: Check Uniqueness of Aliases - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v4 - - 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 '" - - generate-changed-icons-comment: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - - steps: - - uses: actions/checkout@v4 - 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 - with: - files: icons/*.svg - - - name: Find Comment - uses: peter-evans/find-comment@v2 - id: pr-comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: Added or changed icons - - - uses: actions/setup-node@v4 - - name: Install svgson for code preview (safer and faster than installing all deps) - run: npm install svgson - - - name: Generate comment markup - run: node ./scripts/generateChangedIconsCommentMarkup.mjs >> comment-markup.md - id: comment-markup - env: - CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} - - - name: Create or update comment - uses: peter-evans/create-or-update-comment@v3 - with: - comment-id: ${{ steps.pr-comment.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body-path: ./comment-markup.md - edit-mode: replace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfefa436e..e200da21f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,20 +11,19 @@ on: required: true description: Version type: string + tag: + description: NPM tag + default: 'latest' + type: string - workflow_dispatch: - inputs: - version: - description: Version - required: true +permissions: {} jobs: - pre-release: - if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere"]', github.actor) + prepare: + if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere", "jguddas"]', github.actor) runs-on: ubuntu-latest outputs: VERSION: ${{ steps.get_version.outputs.VERSION }} - steps: - name: Get the version id: get_version @@ -37,7 +36,10 @@ jobs: release: if: github.repository == 'lucide-icons/lucide' runs-on: ubuntu-latest - needs: pre-release + needs: prepare + permissions: + id-token: write # Required for OIDC + contents: read strategy: fail-fast: false matrix: @@ -46,31 +48,27 @@ jobs: 'lucide', 'lucide-react', 'lucide-react-native', - 'lucide-vue', - 'lucide-vue-next', - 'lucide-angular', 'lucide-preact', 'lucide-solid', - 'lucide-svelte', + '@lucide/angular', + '@lucide/astro', + '@lucide/icons', + '@lucide/svelte', + '@lucide/vue', ] steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Set Auth Token - run: npm config set //registry.npmjs.org/:_authToken ${{ inputs.NPM_TOKEN || secrets.NPM_TOKEN }} - - name: Set new version - run: pnpm --filter ${{ matrix.package }} version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version + run: pnpm version ${{ needs.prepare.outputs.VERSION }} --filter ${{ matrix.package }} --fail-if-no-match --no-git-tag-version - name: Build run: pnpm --filter ${{ matrix.package }} build @@ -79,31 +77,36 @@ jobs: run: pnpm --filter ${{ matrix.package }} test - name: Publish - run: pnpm --filter ${{ matrix.package }} publish --no-git-checks --ignore-scripts + run: pnpm --filter ${{ matrix.package }} publish --access public --no-git-checks --ignore-scripts --tag ${{ inputs.tag }} + env: + NPM_CONFIG_PROVENANCE: true lucide-static: if: github.repository == 'lucide-icons/lucide' runs-on: ubuntu-latest - needs: [pre-release, lucide-font] + needs: [prepare, lucide-font] + permissions: + id-token: write # Required for OIDC + contents: read + steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - version: 8 - - uses: actions/setup-node@v4 + name: lucide-font + path: lucide-font + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Set Auth Token - run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }} - - name: Set new version - run: pnpm --filter lucide-static version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version + run: pnpm version ${{ needs.prepare.outputs.VERSION }} --filter lucide-static --no-git-tag-version - name: Move Font run: cp -r lucide-font packages/lucide-static/font @@ -112,54 +115,64 @@ jobs: run: pnpm --filter lucide-static build - name: Publish - run: pnpm --filter lucide-static publish --no-git-checks + run: pnpm --filter lucide-static publish --no-git-checks --ignore-scripts --tag ${{ inputs.tag }} --access public + env: + NPM_CONFIG_PROVENANCE: true lucide-font: if: github.repository == 'lucide-icons/lucide' runs-on: ubuntu-latest - needs: pre-release + needs: prepare + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - version: 8 - - uses: actions/setup-node@v4 - with: - node-version: 18 cache: 'pnpm' + node-version-file: 'package.json' - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Outline svg Icons - run: pnpm build:outline-icons + - name: Execute unit tests for build-font + run: pnpm test:font - name: Create font in ./lucide-font - run: pnpm build:font + run: pnpm build:font --saveCodePoints + env: + BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} - name: 'Upload to Artifacts' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: lucide-font path: lucide-font post-release: - if: github.repository == 'lucide-icons/lucide' + if: github.repository == 'lucide-icons/lucide' && github.event_name != 'workflow_call' runs-on: ubuntu-latest - needs: [pre-release, lucide-font] - + needs: [prepare, lucide-font] + permissions: + contents: write # Required to upload the release assets steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - 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.pre-release.outputs.VERSION }}.zip lucide-font - zip -r lucide-icons-${{ needs.pre-release.outputs.VERSION }}.zip icons + 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@v1 + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: - tag_name: ${{ needs.pre-release.outputs.VERSION }} + tag_name: ${{ needs.prepare.outputs.VERSION }} + files: | - lucide-font-${{ needs.pre-release.outputs.VERSION }}.zip - lucide-icons-${{ needs.pre-release.outputs.VERSION }}.zip + lucide-font-${{ needs.prepare.outputs.VERSION }}.zip + lucide-icons-${{ needs.prepare.outputs.VERSION }}.zip diff --git a/.github/workflows/repo-journal.yml b/.github/workflows/repo-journal.yml new file mode 100644 index 000000000..b457fe47f --- /dev/null +++ b/.github/workflows/repo-journal.yml @@ -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" diff --git a/.github/workflows/request-review.yml b/.github/workflows/request-review.yml new file mode 100644 index 000000000..3139435f8 --- /dev/null +++ b/.github/workflows/request-review.yml @@ -0,0 +1,35 @@ +name: Request Review +on: + pull_request: + types: [opened] + paths: + - icons/*.svg + +permissions: {} + +jobs: + request-review: + 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@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@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: icons/*.svg + - run: | + while IFS= read -r file; do + jq -r '.contributors[]' "${file%.svg}.json" + done <<< "$CHANGED_FILES" | while read -r contributor; do + gh pr edit "${{ github.event.pull_request.number }}" --add-reviewer "$contributor" || true + done + env: + GH_TOKEN: ${{ github.token }} + CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.github/workflows/version-up.sh b/.github/workflows/version-up.sh deleted file mode 100755 index 32892478e..000000000 --- a/.github/workflows/version-up.sh +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env bash -## Copyright (C) 2017, Oleksandr Kucherenko -## Last revisit: 2017-09-29 - -## get highest version tag for all branches -function highest_tag(){ - local TAG=$(git describe --tags `git rev-list --tags --max-count=1`) - echo "$TAG" -} - -## extract current branch name -function current_branch(){ - ## expected: heads/{branch_name} - ## expected: {branch_name} - local BRANCH=$(git rev-parse --abbrev-ref HEAD | cut -d"/" -f2) - echo "$BRANCH" -} - -## get latest/head commit hash number -function head_hash(){ - local COMMIT_HASH=$(git rev-parse --verify HEAD) - echo "$COMMIT_HASH" -} - -## extract tag commit hash code, tag name provided by argument -function tag_hash(){ - local TAG_HASH=$(git log -1 --format=format:"%H" $1 2>/dev/null | tail -n1) - echo "$TAG_HASH" -} - -## get latest revision number -function latest_revision(){ - local REV=$(git rev-list --count HEAD 2>/dev/null) - echo "$REV" -} - -## parse last found tag, extract it PARTS -function parse_last(){ - local position=$(($1-1)) - - # two parts found only - local SUBS=( ${PARTS[$position]//-/ } ) - #echo ${SUBS[@]}, size: ${#SUBS} - - # found NUMBER - PARTS[$position]=${SUBS[0]} - #echo ${PARTS[@]} - - # found SUFFIX - if [[ ${#SUBS} -ge 1 ]]; then - PARTS[4]=${SUBS[1],,} #lowercase - #echo ${PARTS[@]}, ${SUBS[@]} - fi -} - -## increment REVISION part, don't touch STAGE -function increment_revision(){ - PARTS[3]=$(( PARTS[3] + 1 )) - IS_DIRTY=1 -} - -## increment PATCH part, reset all other lower PARTS, don't touch STAGE -function increment_patch(){ - PARTS[2]=$(( PARTS[2] + 1 )) - PARTS[3]=0 - IS_DIRTY=1 -} - -## increment MINOR part, reset all other lower PARTS, don't touch STAGE -function increment_minor(){ - PARTS[1]=$(( PARTS[1] + 1 )) - PARTS[2]=0 - PARTS[3]=0 - IS_DIRTY=1 -} - -## increment MAJOR part, reset all other lower PARTS, don't touch STAGE -function incremet_major(){ - PARTS[0]="v$(( PARTS[0] + 1 ))" - PARTS[1]=0 - PARTS[2]=0 - PARTS[3]=0 - IS_DIRTY=1 -} - -## increment the number only of last found PART: REVISION --> PATCH --> MINOR. don't touch STAGE -function increment_last_found(){ - if [[ "${#PARTS[3]}" == 0 || "${PARTS[3]}" == "0" ]]; then - if [[ "${#PARTS[2]}" == 0 || "${PARTS[2]}" == "0" ]]; then - increment_minor - else - increment_patch - fi - else - increment_revision - fi - - # stage part is not EMPTY - if [[ "${#PARTS[4]}" != 0 ]]; then - IS_SHIFT=1 - fi -} - -## compose version from PARTS -function compose(){ - MAJOR="${PARTS[0]}" - MINOR=".${PARTS[1]}" - PATCH=".${PARTS[2]}" - REVISION=".${PARTS[3]}" - SUFFIX="-${PARTS[4]}" - - if [[ "${#PATCH}" == 1 ]]; then # if empty {PATCH} - PATCH="" - fi - - if [[ "${#REVISION}" == 1 ]]; then # if empty {REVISION} - REVISION="" - fi - - if [[ "${PARTS[3]}" == "0" ]]; then # if revision is ZERO - REVISION="" - fi - - # shrink patch and revision - if [[ -z "${REVISION// }" ]]; then - if [[ "${PARTS[2]}" == "0" ]]; then - PATCH=".0" - fi - else # revision is not EMPTY - if [[ "${#PATCH}" == 0 ]]; then - PATCH=".0" - fi - fi - - # remove suffix if we don't have a alpha/beta/rc - if [[ "${#SUFFIX}" == 1 ]]; then - SUFFIX="" - fi - - - echo "${MAJOR}${MINOR}${PATCH}${REVISION}${SUFFIX}" #full format -} - -# initial version used for repository without tags -INIT_VERSION=0.0.0.0-alpha - -# do GIT data extracting -TAG=$(highest_tag) -REVISION=$(latest_revision) -BRANCH=$(current_branch) -TAG_HASH=$(tag_hash $TAG) -HEAD_HASH=$(head_hash) - -# if tag and branch commit hashes are different, than print info about that -#echo $HEAD_HASH vs $TAG_HASH -if [[ "$@" == "" ]]; then - if [[ "$TAG_HASH" == "$HEAD_HASH" ]]; then - echo "Tag $TAG and HEAD are aligned. We will stay on the TAG version." - echo "" - NO_ARGS_VALUE='--stay' - else - PATTERN="^[0-9]+.[0-9]+(.[0-9]+)*(-(alpha|beta|rc))*$" - - if [[ "$BRANCH" =~ $PATTERN ]]; then - echo "Detected version branch '$BRANCH'. We will auto-increment the last version PART." - echo "" - NO_ARGS_VALUE='--default' - else - echo "Detected branch name '$BRANCH' than does not match version pattern. We will increase MINOR." - echo "" - NO_ARGS_VALUE='--minor' - fi - fi -fi - -# -# {MAJOR}.{MINOR}[.{PATCH}[.{REVISION}][-(.*)] -# -# Suffix: alpha, beta, rc -# No Suffix --> {NEW_VERSION}-alpha -# alpha --> beta -# beta --> rc -# rc --> {VERSION} -# -PARTS=( ${TAG//./ } ) -parse_last ${#PARTS[@]} # array size as argument -#echo ${PARTS[@]} - -# if no parameters than emulate --default parameter -if [[ "$@" == "" ]]; then - set -- $NO_ARGS_VALUE -fi - -# parse input parameters -for i in "$@" -do - key="$i" - - case $key in - -a|--alpha) # switched to ALPHA - PARTS[4]="alpha" - IS_SHIFT=1 - ;; - -b|--beta) # switched to BETA - PARTS[4]="beta" - IS_SHIFT=1 - ;; - -c|--release-candidate) # switched to RC - PARTS[4]="rc" - IS_SHIFT=1 - ;; - -r|--release) # switched to RELEASE - PARTS[4]="" - IS_SHIFT=1 - ;; - -p|--patch) # increment of PATCH - increment_patch - ;; - -e|--revision) # increment of REVISION - increment_revision - ;; - -g|--git-revision) # use git revision number as a revision part§ - PARTS[3]=$(( REVISION )) - IS_DIRTY=1 - ;; - -i|--minor) # increment of MINOR by default - increment_minor - ;; - --default) # stay on the same stage, but increment only last found PART of version code - increment_last_found - ;; - -m|--major) # increment of MAJOR - incremet_major - ;; - -s|--stay) # extract version info - IS_DIRTY=1 - NO_APPLY_MSG=1 - ;; - -t|--tag-only) # extract version info - TAG_ONLY=1 - ;; - --apply) - DO_APPLY=1 - ;; - -h|--help) - help - ;; - esac - shift -done - -# detected shift, but no increment -if [[ "$IS_SHIFT" == "1" ]]; then - # temporary disable stage shift - stage=${PARTS[4]} - PARTS[4]='' - - # detect first run on repository, INIT_VERSION was used - if [[ "$(compose)" == "0.0" ]]; then - increment_minor - fi - - PARTS[4]=$stage -fi - -# no increment applied yet and no shift of state, do minor increase -if [[ "$IS_DIRTY$IS_SHIFT" == "" ]]; then - increment_minor -fi - -compose - -# is proposed tag in conflict with any other TAG -PROPOSED_HASH=$(tag_hash $(compose)) -if [[ "${#PROPOSED_HASH}" -gt 0 && "$NO_APPLY_MSG" == "" ]]; then - echo -e "\033[31mERROR:\033[0m " - echo -e "\033[31mERROR:\033[0m Found conflict with existing tag \033[32m$(compose)\033[0m / $PROPOSED_HASH" - echo -e "\033[31mERROR:\033[0m Only manual resolving is possible now." - echo -e "\033[31mERROR:\033[0m " - echo -e "\033[31mERROR:\033[0m To Resolve try to add --revision or --patch modifier." - echo -e "\033[31mERROR:\033[0m " - echo "" - exit 1 -fi diff --git a/.gitignore b/.gitignore index a8a38a895..0ad3cb108 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .obsidian .now .idea +.env node_modules dist build @@ -13,18 +14,32 @@ coverage stats *.log outlined +lucide-font packages/**/src/icons/*.js packages/**/src/icons/*.ts packages/**/src/icons/*.tsx +packages/**/src/aliases/*.ts packages/**/src/aliases.ts +!packages/**/src/aliases/index.ts +packages/**/src/utils/*.ts +!packages/shared/src/utils/*.ts packages/**/src/dynamicIconImports.ts +packages/**/DynamicIcon.d.ts packages/**/dynamicIconImports.js +packages/**/dynamicIconImports.mjs packages/**/dynamicIconImports.d.ts +packages/**/dynamicIconImports.d.mts packages/**/dynamicIconImports.js.map +packages/**/dynamicIconImports.mjs.map +packages/**/dynamic.d.ts +packages/**/dynamic.d.mts +packages/**/dynamic.mjs.map +packages/**/dynamic.mjs packages/**/LICENSE categories.json tags.json .vercel +package-lock.json # docs docs/.vitepress/cache @@ -33,9 +48,16 @@ docs/.vitepress/.temp docs/.vitepress/data/iconNodes docs/.vitepress/data/iconMetaData.ts docs/.vitepress/data/releaseMetaData.json -docs/.vitepress/data/releaseMetaData +docs/.vitepress/data/releaseMetadata docs/.vitepress/data/categoriesData.json 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 diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 3e775efb0..000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -auto-install-peers=true diff --git a/.prettierignore b/.prettierignore index c150f271b..435726b81 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,8 +7,8 @@ docs/.vitepress/cache docs/.vitepress/data docs/.nitro -# lucide-angular -packages/lucide-angular/.angular/cache +# @lucide/angular +packages/angular/.angular # lucide-static packages/lucide-static/icons @@ -17,7 +17,8 @@ packages/lucide-static/sprite.svg packages/lucide-static/tags.json packages/lucide-static/icon-nodes.json packages/lucide-static/font +packages/svelte/.svelte-kit # lucide-svelte -packages/lucide-svelte/src/icons/*.svelte -packages/lucide-svelte/.svelte-kit +packages/svelte/src/icons/*.svelte +packages/svelte/.svelte-kit diff --git a/.vscode/settings.json b/.vscode/settings.json index f179cbc07..9c7cfd29c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { - "cSpell.words": ["devs", "preact", "Preact"], + "cSpell.words": ["devs", "linecap", "linejoin", "preact", "Preact"], "eslint.enable": true, "eslint.validate": ["javascript", "svg"], - "svg.preview.background": "transparent" + "svg.preview.background": "editor" } diff --git a/.vscode/svg.code-snippets b/.vscode/svg.code-snippets index 52cbb23c1..dbf726e6b 100644 --- a/.vscode/svg.code-snippets +++ b/.vscode/svg.code-snippets @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/Yash-Singh1/vscode-snippets-json-schema/main/schema.json", "Lucide SVG": { "scope": "xml", "description": "Base SVG with Lucide attributes.", @@ -51,6 +52,16 @@ ], "body": "" }, + "Squircle": { + "scope": "xml", + "description": "SVG `path` with Lucide defaults.", + "prefix": [ + "squircle", + "path", + "" + }, "Ellipse": { "scope": "xml", "description": "SVG `ellipse`.", diff --git a/BRAND_LOGOS_STATEMENT.md b/BRAND_LOGOS_STATEMENT.md new file mode 100644 index 000000000..6a7404c79 --- /dev/null +++ b/BRAND_LOGOS_STATEMENT.md @@ -0,0 +1,73 @@ +# Brand Logos Statement + +Our official stance on including brand logos in Lucide + +## TL;DR + +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**. + +If you need brand logos, we recommend [Simple Icons](https://simpleicons.org/), which provides an extensive, legally safer collection of brand marks. + +--- + +## 1. Historical Context + +This is not a new debate — other icon sets have gone through the same discussion: + +- **Material Design Icons** [deprecated all brand icons](https://github.com/Templarian/MaterialDesign/issues/6602) because they didn't fit the style, didn't work well in one color, and often looked out of place in a 24×24px grid. +- **Feather Icons** [came to the same conclusion](https://github.com/feathericons/feather/issues/763): brand logos have their own style, and forcing them into another inevitably leads to aesthetic compromises. +- **Lucide** learned from these examples — we'd rather focus on making a consistent set of non-brand icons that all work together. + +## 2. Legal Considerations + +Most brand logos: +- Are **protected by trademark or copyright**. +- Have **strict rules** for how they can be used (colors, spacing, proportions, etc.). +- **Don't allow modification** — but we'd have to change them to fit Lucide's style. + +This means adding them could: +1. Break copyright or trademark law. +2. Make both you and the Lucide project legally responsible. +3. Force us to review every new request one by one for legal issues — something we simply can't do. + +> **Note:** Simple Icons avoids this by keeping logos exactly as the brand provides them — though even they sometimes face [legal challenges](https://github.com/simple-icons/simple-icons/issues/11236). + +## 3. Design & Consistency + +Lucide is all about **visual consistency**. + +Adding brand logos would: +- Break [our own design rules](https://lucide.dev/contribute/icon-design-guide#icon-design-principles) for shapes, proportions, and stroke. +- Mix two fundamentally different categories of graphics (pictograms vs. corporate logos). +- Create a library where a subset of icons will always look "out of place". + +If the logos are not in Lucide's style, why include them in Lucide at all? Better to use them from a dedicated brand icon source. + +## 4. Maintenance Burden + +Even with our current **"no brand icon requests"** policy, people still request them regularly. + +Having any brand icons in the set: +- Makes people think we might add more in the future. +- Leads to repeated requests and the same conversations over and over. +- Wastes maintainer time redirecting people to the same explanation. + +Removing them entirely solves this problem. + +## 5. Recommended Alternatives + +If you need brand icons, try: +- [Simple Icons](https://simpleicons.org/): offers a huge range of brands, in consistent SVG format, using a 24×24 viewBox, the same as ours. +- Official brand asset pages: most major companies provide downloadable SVGs. + +You can use these alongside Lucide without bloating our core library. + +## Final Words + +Lucide is an **icon** set, not a **logo** set. + +Logos belong in dedicated logo resources. + +We're focusing on what Lucide does best: providing a clean, cohesive, and legally safe collection of open-source icons. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index bd8e14ad0..da613c8d4 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,74 +1,90 @@ # Contributor Covenant Code of Conduct +This Code of Conduct outlines our expectations for participant behavior as well as the consequences for unacceptable behavior within our community. We are committed to providing a welcoming, safe, and inclusive environment for all members of our community. + ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +We pledge to make our community welcoming, safe, and equitable for all. -## Our Standards +We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. -Examples of behavior that contributes to creating a positive environment -include: +## Encouraged Behaviors -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. -Examples of unacceptable behavior by participants include: +With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +1. Respecting the **purpose of our community**, our activities, and our ways of gathering. +2. Engaging **kindly and honestly** with others. +3. Respecting **different viewpoints** and experiences. +4. **Taking responsibility** for our actions and contributions. +5. Gracefully giving and accepting **constructive feedback**. +6. Committing to **repairing harm** when it occurs. +7. Behaving in other ways that promote and sustain the **well-being of our community**. -## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +## Restricted Behaviors + +We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. + +1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. +2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. +3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. +4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. +5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. +6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. +7. Behaving in other ways that **threaten the well-being** of our community. + +### Other Restrictions + +1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. +2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. +3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. +4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. + + +## Reporting an Issue + +Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. + +When an incident does occur, it is important to report it promptly. To report a possible violation, email: [info@lucide.dev](mailto:info@lucide.dev) + +Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. + + +## Addressing and Repairing Harm + +If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. + +1) Warning + 1) Event: A violation involving a single incident or series of incidents. + 2) Consequence: A private, written warning from the Community Moderators. + 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. +2) Temporarily Limited Activities + 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. + 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. + 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. +3) Temporary Suspension + 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. + 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. + 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. +4) Permanent Ban + 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. + 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. + 3) Repair: There is no possible repair in cases of this severity. + +This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at johnletey@gmail.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) + +For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 35b3a382d..ac7ea7869 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,4 @@ - -# Contribution Guidelines +# Contribution Guide :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: @@ -16,37 +15,43 @@ Guidelines for pull requests: - __Make your commit messages as descriptive as possible.__ Include as much information as you can. Explain anything that the file diffs themselves won’t make apparent. - __Document your pull request__. Explain your fix, link to the relevant issue, add screenshots when adding new icons. -- __Make sure the target of your pull request is the relevant branch__. Most of bugfix or new feature should go to the `main` branch. -- __Include only related work__. If your pull request has unrelated commit, it won't be accepted. +- __Make sure the target of your pull request is the relevant branch__. Most of bug fixes or new feature should go to the `main` branch. +- __Include only related work__. If your pull request has unrelated commits, it won't be accepted. -### Pull Requests Including Icons +### Contributing Icons + +We love contributions of new icons from the community! If you want to contribute new icons, please follow the guidelines below. #### Guidelines Please make sure you follow the icon guidelines, that should be followed to keep quality and consistency when making icons for Lucide. -Read it here: [ICON_GUIDELINES](https://lucide.dev/docs/icon-design-guide). +Read it here: [ICON_GUIDELINES](https://lucide.dev/contribute/icon-design-guide). -### Editor guides +#### Lucide Studio + +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 Here you can find instructions on how to implement the guidelines with different vector graphics editors: -#### [Adobe Illustrator Guide](https://lucide.dev/docs/illustrator-guide) +##### [Adobe Illustrator Guide](https://lucide.dev/contribute/illustrator-guide) -You can also [download an Adobe Illustrator template](https://github.com/lucide-icons/lucide/blob/main/docs/public/templates/illustrator_template.ai). +You can also [download an Adobe Illustrator template](https://lucide.dev/templates/illustrator_template.ai). -#### [Inkscape Guide](https://lucide.dev/docs/inkscape-guide) +##### [Inkscape Guide](https://lucide.dev/contribute/inkscape-guide) -#### [Figma Guide](https://lucide.dev/docs/figma-guide) +##### [Figma Guide](https://lucide.dev/contribute/figma-guide) -#### [Affinity Designer Guide](https://lucide.dev/guide/design/affinity-designer-guide) +##### [Affinity Designer Guide](https://lucide.dev/contribute/affinity-designer-guide) -### Submitting Multiple Icons +#### Submitting Multiple Icons -If you want submit multiple icons, please separate the icons and group them. That makes reviewing the icons easier and keep the thread clean and scoped. -So don't submit multiple icons in one PR that have noting to do with each other. +If you want to submit multiple icons, please separate the icons and group them. That makes reviewing the icons easier and keeps the thread clean and scoped. +So don't submit multiple icons in one PR that have nothing to do with each other. So for example don't create one PR with icons: `arrow-up`, `bicycle`, `arrow-down`. -Seperate them by two PRs; 'pr-01' `arrow`, `arrow-down` and 'pr-02' `bicycle`. +Separate them into two PRs; 'pr-01' `arrow`, `arrow-down` and 'pr-02' `bicycle`. ## Icon Requests @@ -72,12 +77,12 @@ pnpm install # Install dependencies, including the workspace packages ### Packages -> PNPM Workspaces -To distribute different packages we use PNPM workspaces. Before you start make sure you are familiar with this concept. The concept of working in workspaces is created by Yarn, they have a well written introduction: [yarn workspaces](https://classic.yarnpkg.com/lang/enhttps://lucide.dev/docs/workspaces). +To distribute different packages we use [PNPM workspaces](https://pnpm.io/workspaces). Before you start make sure you are familiar with this concept. The concept of working in workspaces is created by Yarn, they have a well written introduction: [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces). The configured directory for workspaces is the [packages](https://github.com/lucide-icons/lucide/tree/main/packages) directory, located in the root directory. There you will find all the current packages from lucide. There are more workspaces defined, see [`pnpm-workspace.yaml`](https://github.com/lucide-icons/lucide/blob/main/pnpm-workspace.yaml). -> Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and used pub for publishing. +> Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and uses pub for publishing. ### Generated Code @@ -127,7 +132,7 @@ When adding new features to for example the icon component for a framework. It i ### Local Testing -To test changes in a local project, you can use `yarn link`, `npm link` or `pnpm link` to link the package. Before you do this make sure you builded the package first. +To test changes in a local project, you can use `yarn link`, `npm link`, `bun link` or `pnpm link` to link the package. Before you do this make sure you've built the package first. ```sh # in packages/lucide-react @@ -157,23 +162,45 @@ lucide The lucide.dev website is using [vitepress](https://vitepress.dev/) to generate the static website. The markdown files are located in the docs directory. +#### Running the Docs Website Locally + +To test the docs website locally, follow these steps: + +1. **Navigate to the docs directory** + +```sh +cd docs +``` + +2. **Start the Local Development Server** + +```sh + pnpm run docs:dev +``` + +3. **Open the Website Locally** + +Vitepress should open with the following format: + +VitePress dev server is running at: +- **Local**: `http://localhost:3000/` +- **Network**: `http://192.168.x.x:3000/` + ### Guides Detailed documentation about: installation, guides, packages, design guides etc. ### Icons -All the icons of lucide in SVG format. These will be used as source for all the packages and other distributions for the lucide icons. +All the icons of Lucide in SVG format. These will be used as source for all the packages and other distributions for the Lucide icons. -### packages +### Packages Includes all the (npm) packages of lucide. -> Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and used pub for publishing. +### Scripts -### scripts - -Includes usefully scripts to automate certain jobs. Big part of the scripts is the template generation, for example it generates icon components for all the packages. These scripts are usually executed from the "scripts" section in the package.json. +Includes useful scripts to automate certain jobs. Big part of the scripts is the template generation, for example it generates icon components for all the packages. These scripts are usually executed from the "scripts" section in the package.json. ## Documentation @@ -190,4 +217,4 @@ If you need any help or have problems with you contribution. Please don't hesita Thank you to all the people who already contributed to Lucide! - + diff --git a/LICENSE b/LICENSE index 325e8ff0a..718bb3f0e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. +Copyright (c) 2026 Lucide Icons and Contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -13,3 +13,31 @@ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6076562c3..a33e9026d 100644 --- a/README.md +++ b/README.md @@ -7,277 +7,80 @@

- license - npm package - figma installs - build status + license + figma installs + build status discord chat

+

+ Icons + · + Guide + · + Packages + · + License + · + Showcase +

# Lucide -Community-run fork of [Feather Icons](https://github.com/feathericons/feather), open for anyone to contribute icons. +Lucide is an open-source icon library that provides 1600+ vector (svg) files for displaying icons and symbols in digital and non-digital projects. The library aims to make it easier for designers and developers to incorporate icons into their projects by providing several official [packages](https://lucide.dev/packages) to make it easier to use these icons in your project. -It began after growing dissatisfaction with the [Feather Icons](https://github.com/feathericons/feather) project moderation. With over 300+ open issues and over 100+ open PRs, the Feather Icons project has been abandoned. This unfortunately means that hundreds of developers and designers wasted their time contributing to Feather Icons with no chance of PRs being accepted. +## Packages -Lucide is trying to expand the icon set as much as possible while staying faithful to the original simplistic design language. We do this as a community of devs and designers and hope that you'll join us! - -### Why choose Lucide over Feather Icons - -- More icons to work with: Lucide already has hundreds of icons more than Feather does. -- Official libraries and integrations with popular frameworks and design tools. -- Well maintained code base. -- Active community, regularly growing and improving the set. - -## Table of Contents - -- [Usage](#usage) - - [Web](#web) - - [React](#react) - - [React Native](#react-native) - - [Vue 2](#vue-2) - - [Vue 3](#vue-3) - - [Angular](#angular) - - [Preact](#preact) - - [Static (svg sprite, font, icons ..)](#static-svg-sprite-font-icons-) - - [Figma](#figma) - - [Laravel](#laravel) - - [Svelte](#svelte) - - [Solid](#solid) - - [Hyva](#hyva) - - [Eleventy](#eleventy) -- [Contributing](#contributing) -- [Community](#community) -- [License](#license) -- [Credits](#credits) -- [Sponsors](#sponsors) - -## Usage - -At its core, Lucide is a collection of [SVG](https://svgontheweb.com/#svg) files. This means that you can use Lucide icons in all the same ways you can use SVGs (e.g. `img`, `background-image`, `inline`, `object`, `embed`, `iframe`). Here's a helpful article detailing the many ways SVGs can be used on the web: [SVG on the Web – Implementation Options](https://svgontheweb.com/#implementation) - -The following are additional ways you can use Lucide. -With the Javascript library you can easily incorporate the icon you want in your webpage. - -### Web - -Implementation of the lucide icon library for web applications. - -```sh -npm install lucide -``` - -or - -```sh -yarn add lucide -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide#lucide). - -### React - -Implementation of the lucide icon library for react applications. - -```sh -yarn add lucide-react -``` - -or - -```sh -npm install lucide-react -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-react#lucide-react). - -### React Native - -Implementation of the lucide icon library for React Native applications. - -```sh -yarn add lucide-react-native -``` - -or - -```sh -npm install lucide-react-native -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-react-native#lucide-react-native). - -### Vue 2 - -Implementation of the lucide icon library for vue applications. - -```sh -yarn add lucide-vue -``` - -or - -```sh -npm install lucide-vue -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-vue#lucide-vue). - -### Vue 3 - -Implementation of the lucide icon library for vue applications. - -```sh -yarn add lucide-vue-next -``` - -or - -```sh -npm install lucide-vue-next -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-vue-next#lucide-vue-next). - -### Angular - -```sh -yarn add lucide-angular -``` - -or - -```sh -npm install lucide-angular -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-angular#lucide-angular). - -### Preact - -Implementation of the lucide icon library for preact applications. - -```sh -yarn add lucide-preact -``` - -or - -```sh -npm install lucide-preact -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-preact#lucide-preact). - -### Static (svg sprite, font, icons ..) - -Assets: -[Font Files](https://github.com/lucide-icons/lucide/releases/latest) -[SVG Files](https://github.com/lucide-icons/lucide/releases/latest) -[SVG Sprite](https://cdn.jsdelivr.net/npm/lucide-static@latest/sprite.svg) - -NPM package - -```sh -yarn add lucide-static -``` - -or - -```sh -npm install lucide-static -``` +| Logo | Package | Version | Downloads | Links | +| ---- | ------- | ------- | --------- | ----- | +| JS logo | **`lucide`** | [![npm](https://img.shields.io/npm/v/lucide)](https://www.npmjs.com/package/lucide) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide) | [Docs](https://lucide.dev/guide/lucide) · [Source](./packages/lucide) | +| React logo | **`lucide-react`** | [![npm](https://img.shields.io/npm/v/lucide-react)](https://www.npmjs.com/package/lucide-react) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-react) | [Docs](https://lucide.dev/guide/react) · [Source](./packages/lucide-react) | +| Vue logo | **`@lucide/vue`** | [![npm](https://img.shields.io/npm/v/@lucide/vue)](https://www.npmjs.com/package/@lucide/vue) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/vue) | [Docs](https://lucide.dev/guide/vue) · [Source](./packages/vue) | +| Svelte logo | **`@lucide/svelte`** | [![npm](https://img.shields.io/npm/v/@lucide/svelte)](https://www.npmjs.com/package/@lucide/svelte) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/svelte) | [Docs](https://lucide.dev/guide/svelte) · [Source](./packages/svelte) | +| Solid logo | **`lucide-solid`** | [![npm](https://img.shields.io/npm/v/lucide-solid)](https://www.npmjs.com/package/lucide-solid) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-solid) | [Docs](https://lucide.dev/guide/solid) · [Source](./packages/lucide-solid) | +| Preact logo | **`lucide-preact`** | [![npm](https://img.shields.io/npm/v/lucide-preact)](https://www.npmjs.com/package/lucide-preact) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-preact) | [Docs](https://lucide.dev/guide/preact) · [Source](./packages/lucide-preact) | +| React Native logo | **`lucide-react-native`** | [![npm](https://img.shields.io/npm/v/lucide-react-native)](https://www.npmjs.com/package/lucide-react-native) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-react-native) | [Docs](https://lucide.dev/guide/react-native) · [Source](./packages/lucide-react-native) | +| Angular logo | **`@lucide/angular`** | [![npm](https://img.shields.io/npm/v/@lucide/angular)](https://www.npmjs.com/package/@lucide/angular) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/angular) | [Docs](https://lucide.dev/guide/angular) · [Source](./packages/angular) | +| Astro logo | **`@lucide/astro`** | [![npm](https://img.shields.io/npm/v/@lucide/astro)](https://www.npmjs.com/package/@lucide/astro) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/astro) | [Docs](https://lucide.dev/guide/astro) · [Source](./packages/astro) | +| SVG logo | **`lucide-static`** | [![npm](https://img.shields.io/npm/v/lucide-static)](https://www.npmjs.com/package/lucide-static) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-static) | [Docs](https://lucide.dev/guide/static) · [Source](./packages/lucide-static) | ### Figma -The lucide figma plugin. +The Lucide Figma plugin. Visit [Figma community page](https://www.figma.com/community/plugin/939567362549682242/Lucide-Icons) to install the plugin. Figma Lucide Cover -### Laravel - -Implementation of Lucide icon's using `blade-icons` for Laravel based projects. - -```sh -composer require mallardduck/blade-lucide-icons -``` - -For more details, see the [documentation](https://github.com/mallardduck/blade-lucide-icons/blob/main/README.md). - -### Svelte - -Implementation of the lucide icon library for Svelte applications. - -```sh -yarn add lucide-svelte -``` - -or - -```sh -npm install lucide-svelte -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-svelte#lucide-svelte). - -### Solid - -Implementation of the lucide icon library for solid applications. - -```sh -yarn add lucide-solid -``` - -or - -```sh -npm install lucide-solid -``` - -For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-solid#lucide-solid). - -### Hyva - -Implementation of Lucide icon's using Hyvä's svg php viewmodal to render icons for Magento 2 Hyva theme based projects. - -```sh -composer require siteation/magento2-hyva-icons-lucide -``` - -For more details, see the [documentation](https://github.com/Siteation/magento2-hyva-icons-lucide/blob/main/README.md). - -### Eleventy - -Using this plugin, Eleventy projects can incorporate Lucide icons. it makes it simple to use Lucide icons into your themes via shortcodes, improving your website's overall usability and visual appeal. - -```sh -npm install @grimlink/eleventy-plugin-lucide-icons -``` - -For more details, see the [documentation](https://github.com/GrimLink/eleventy-plugin-lucide-icons/blob/main/README.md). - ## Contributing For more info on how to contribute please see the [contribution guidelines](https://github.com/lucide-icons/lucide/blob/main/CONTRIBUTING.md). Caught a mistake or want to contribute to the documentation? [Edit this page on Github](https://github.com/lucide-icons/lucide/blob/main/README.md) +## About brand logos + +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) + ## Community Join the community on our [Discord](https://discord.gg/EH6nSts) server! ## License -Lucide is totally free for commercial use and personally use, this software is licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). +Lucide is totally free for commercial use and personal use, this software is licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). ## Credits Thank you to all the people who contributed to Lucide! - + + + + +[//]: <> (Sponsors) ## Sponsors @@ -287,6 +90,19 @@ Thank you to all the people who contributed to Lucide! DigitalOcean Referral Badge -### Awesome backer 🍺 +### Hero backers 🦸 -Scipress sponsor badge +Zephyr Cloud – From idea to prod: fast micro-frontend delivery! + +### Awesome backers 🍺 + +pdfme – Open-source PDF generation library built with TypeScript and React. +Pax Historia – An alternate history sandbox game + +### Backers ☕ + +Fina Money – Modular Finance Tracker + +### Other contributors 💸 + +You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons). diff --git a/brand-stopwords.json b/brand-stopwords.json new file mode 100644 index 000000000..04816fa5c --- /dev/null +++ b/brand-stopwords.json @@ -0,0 +1,148 @@ +{ + "adobe": "Adobe", + "airplay": "AirPlay", + "amazon": "Amazon", + "android": "Android", + "angular": "Angular", + "aws": "AWS", + "azure": "Azure", + "bandcamp": "Bandcamp", + "behance": "Behance", + "bitbucket": "Bitbucket", + "blender": "Blender", + "bluesky": "BlueSky", + "bootstrap": "Bootstrap", + "brave": "Brave", + "chakra": "Chakra UI", + "chrome": "Chrome", + "codepen": "Codepen", + "codesandbox": "CodeSandbox", + "csharp": "C#", + "cypress": "Cypress", + "dart": "Dart", + "deezer": "Deezer", + "deno": "Deno", + "discord": "Discord", + "docker": "Docker", + "dribbble": "Dribbble", + "dropbox": "Dropbox", + "edge": "Edge", + "ember": "Ember", + "epic": "Epic Games", + "erlang": "Erlang", + "esbuild": "esbuild", + "eslint": "ESLint", + "facebook": "Facebook", + "figjam": "FigJam", + "figma": "Figma", + "firebase": "Firebase", + "firefox": "Firefox", + "framer": "Framer", + "gatsby": "Gatsby", + "gcp": "Google Cloud", + "github": "GitHub", + "gitlab": "GitLab", + "golang": "GoLang", + "google": "Google", + "gmail": "Gmail", + "gravatar": "Gravatar", + "haskell": "Haskell", + "instagram": "Instagram", + "java": "Java", + "javascript": "JavaScript", + "jest": "Jest", + "jira": "Jira", + "kotlin": "Kotlin", + "kubernetes": "Kubernetes", + "leetcode": "LeetCode", + "leet-code": "LeetCode", + "linkedin": "LinkedIn", + "lua": "Lua", + "mariadb": "MariaDB", + "mcp": "MCP", + "messenger": "Messenger", + "microsoft": "Microsoft", + "mongodb": "MongoDB", + "mui": "Material UI", + "mysql": "MySQL", + "nestjs": "NestJS", + "netflix": "Netflix", + "netlify": "Netlify", + "nextjs": "Next.js", + "nodejs": "Node.js", + "notion": "Notion", + "nostr": "Nostr", + "npm": "npm", + "nuxt": "Nuxt", + "opera": "Opera", + "oracle": "Oracle", + "patreon": "Patreon", + "paypal": "PayPal", + "perl": "Perl", + "php": "PHP", + "pinterest": "Pinterest", + "pix": "PiX", + "playstation": "PlayStation", + "playwright": "Playwright", + "pnpm": "pnpm", + "postcss": "PostCSS", + "postgresql": "PostgreSQL", + "prettier": "Prettier", + "prisma": "Prisma", + "python": "Python", + "qwik": "Qwik", + "react": "React", + "reddit": "Reddit", + "redis": "Redis", + "rollup": "Rollup", + "rust": "Rust", + "safari": "Safari", + "sass": "Sass", + "scala": "Scala", + "scss": "Sass", + "semantic": "Semantic UI", + "shopify": "Shopify", + "skype": "Skype", + "slack": "Slack", + "snapchat": "Snapchat", + "solidjs": "SolidJS", + "soundcloud": "SoundCloud", + "spotify": "Spotify", + "sqlite": "SQLite", + "squarespace": "Squarespace", + "steam": "Steam", + "stripe": "Stripe", + "substack": "Substack", + "supabase": "Supabase", + "surge": "Surge", + "svelte": "Svelte", + "swift": "Swift", + "tailwind": "Tailwind CSS", + "telegram": "Telegram", + "terraform": "Terraform", + "tesla": "Tesla", + "tidal": "Tidal", + "tiktok": "TikTok", + "trello": "Trello", + "twitch": "Twitch", + "twitter": "Twitter", + "typescript": "TypeScript", + "unity": "Unity", + "unreal": "Unreal Engine", + "vercel": "Vercel", + "vimeo": "Vimeo", + "vite": "Vite", + "vitest": "Vitest", + "vue": "Vue", + "webpack": "Webpack", + "wechat": "WeChat", + "whatsapp": "WhatsApp", + "windows": "Windows", + "wix": "Wix", + "x.com": "X.com", + "x-social": "X.com", + "xbox": "Xbox", + "yarn": "Yarn", + "youtube": "YouTube", + "zig": "Zig" +} diff --git a/categories/brands.json b/categories/brands.json deleted file mode 100644 index c48c4b3b0..000000000 --- a/categories/brands.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../category.schema.json", - "title": "Brands", - "icon": "facebook" -} diff --git a/categories/charts.json b/categories/charts.json index 22beb849e..e9bdc1882 100644 --- a/categories/charts.json +++ b/categories/charts.json @@ -1,5 +1,5 @@ { "$schema": "../category.schema.json", "title": "Charts", - "icon": "pie-chart" + "icon": "chart-pie" } diff --git a/categories/currency.json b/categories/currency.json deleted file mode 100644 index 0879774d1..000000000 --- a/categories/currency.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../category.schema.json", - "title": "Currency", - "icon": "dollar-sign" -} diff --git a/categories/emoji.json b/categories/emoji.json index 78442db1d..2bd590877 100644 --- a/categories/emoji.json +++ b/categories/emoji.json @@ -1,5 +1,5 @@ { "$schema": "../category.schema.json", "title": "Emoji", - "icon": "smile" + "icon": "face-slightly-smiling" } diff --git a/categories/money.json b/categories/finance.json similarity index 75% rename from categories/money.json rename to categories/finance.json index fd706dee5..267643671 100644 --- a/categories/money.json +++ b/categories/finance.json @@ -1,5 +1,5 @@ { "$schema": "../category.schema.json", - "title": "Money", + "title": "Finance", "icon": "piggy-bank" } diff --git a/categories/furniture.json b/categories/furniture.json deleted file mode 100644 index 56cf3fa30..000000000 --- a/categories/furniture.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../category.schema.json", - "title": "Furniture", - "icon": "rocking-chair" -} diff --git a/categories/maps.json b/categories/maps.json deleted file mode 100644 index 1a948f119..000000000 --- a/categories/maps.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "../category.schema.json", - "title": "Maps", - "icon": "map" -} diff --git a/categories/maths.json b/categories/math.json similarity index 70% rename from categories/maths.json rename to categories/math.json index f80691908..1ae72ad9e 100644 --- a/categories/maths.json +++ b/categories/math.json @@ -1,5 +1,5 @@ { "$schema": "../category.schema.json", - "title": "Maths", + "title": "Mathematics", "icon": "divide" } diff --git a/categories/navigation.json b/categories/navigation.json index 0cb9e209d..0c46693cb 100644 --- a/categories/navigation.json +++ b/categories/navigation.json @@ -1,5 +1,5 @@ { "$schema": "../category.schema.json", - "title": "Navigation", + "title": "Navigation & Places", "icon": "compass" } diff --git a/categories/notifications.json b/categories/notifications.json index 8ef569fd0..c2a729bec 100644 --- a/categories/notifications.json +++ b/categories/notifications.json @@ -1,5 +1,5 @@ { "$schema": "../category.schema.json", - "title": "Notifications", + "title": "Notification", "icon": "triangle-alert" } diff --git a/tools/build-font/styles/.gitkeep b/comment-markup.md similarity index 100% rename from tools/build-font/styles/.gitkeep rename to comment-markup.md diff --git a/docs/.vitepress/api/categories/index.get.ts b/docs/.vitepress/api/categories/index.get.ts index 76e1f95de..0ac20a3e5 100644 --- a/docs/.vitepress/api/categories/index.get.ts +++ b/docs/.vitepress/api/categories/index.get.ts @@ -1,3 +1,4 @@ +import { eventHandler, setResponseHeader } from 'h3'; import iconMetaData from '../../data/iconMetaData'; export default eventHandler((event) => { diff --git a/docs/.vitepress/api/figma/data.ts b/docs/.vitepress/api/figma/data.ts index 1c938c998..cde37f140 100644 --- a/docs/.vitepress/api/figma/data.ts +++ b/docs/.vitepress/api/figma/data.ts @@ -1,3 +1,4 @@ +import { eventHandler, setResponseHeader } from 'h3'; import iconNodes from '../../data/iconNodes/index.ts'; import { IconNodeWithKeys } from '../../theme/types'; import iconMetaData from '../../data/iconMetaData'; diff --git a/docs/.vitepress/api/gh-icon/[...data].get.ts b/docs/.vitepress/api/gh-icon/[...data].get.tsx similarity index 62% rename from docs/.vitepress/api/gh-icon/[...data].get.ts rename to docs/.vitepress/api/gh-icon/[...data].get.tsx index 346901712..b74ce7dc3 100644 --- a/docs/.vitepress/api/gh-icon/[...data].get.ts +++ b/docs/.vitepress/api/gh-icon/[...data].get.tsx @@ -1,6 +1,6 @@ import { eventHandler, setResponseHeader, defaultContentType } from 'h3'; import { renderToString, renderToStaticMarkup } from 'react-dom/server'; -import { createElement } from 'react'; +import React from 'react'; import SvgPreview from '../../lib/SvgPreview/index.tsx'; import iconNodes from '../../data/iconNodes'; import createLucideIcon from 'lucide-react/src/createLucideIcon'; @@ -13,10 +13,12 @@ export default eventHandler((event) => { const data = pathData.at(-1).slice(0, -4); const [name] = pathData; - const src = Buffer.from(data, 'base64') - .toString('utf8') - .replaceAll('\n', '') - .replace(/]*>|<\/svg>/g, ''); + const src = Buffer.from(data, 'base64').toString('utf8').replaceAll('\n', ''); + + const width = parseInt((src.includes(' { .map((_, idx, arr) => arr.slice(0, idx + 1).join('-')) .reverse() .find((groupName) => groupName in iconNodes); - if (backdropName) { + if (!(name in iconNodes) && backdropName) { const iconNode = iconNodes[backdropName]; const LucideIcon = createLucideIcon(backdropName, iconNode); - const svg = renderToStaticMarkup(createElement(LucideIcon)); + const svg = renderToStaticMarkup(); const backdropString = svg.replaceAll('\n', '').replace(/]*>|<\/svg>/g, ''); children.push( - createElement(Backdrop, { - backdropString, - src, - color: name in iconNodes ? 'red' : '#777', - }), + ]*>|<\/svg>/g, '')} + color="#777" + />, ); - if (name in iconNodes) { - children.push( - createElement(Backdrop, { - backdropString: src, - src: backdropString, - color: 'lime', - }), - ); - } } - const svg = Buffer.from( - // We can't use jsx here, is not supported here by nitro. - renderToString(createElement(SvgPreview, { src, showGrid: true }, children)), - ).toString('utf8'); - defaultContentType(event, 'image/svg+xml'); setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000'); - return svg; + return renderToString( + ]*>|<\/svg>/g, '')} + showGrid + height={height} + width={width} + > + {children} + , + ); }); diff --git a/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx b/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx new file mode 100644 index 000000000..34d15b684 --- /dev/null +++ b/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx @@ -0,0 +1,51 @@ +import { eventHandler, setResponseHeader, defaultContentType } from 'h3'; +import { renderToString, renderToStaticMarkup } from 'react-dom/server'; +import React from 'react'; +import Diff from '../../../lib/SvgPreview/Diff.tsx'; +import iconNodes from '../../../data/iconNodes'; +import createLucideIcon from 'lucide-react/src/createLucideIcon'; + +export default eventHandler((event) => { + const { params } = event.context; + + const pathData = params.data.split('/'); + const data = pathData.at(-1).slice(0, -4); + const [name] = pathData; + + const newSrc = Buffer.from(data, 'base64') + .toString('utf8') + .replaceAll('\n', '') + .replace(/]*>|<\/svg>/g, ''); + + const width = parseInt( + (newSrc.includes(') + .replaceAll('\n', '') + .replace(/]*>|<\/svg>/g, '') + : ''; + + defaultContentType(event, 'image/svg+xml'); + setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000'); + + return renderToString( + + {children} + , + ); +}); diff --git a/docs/.vitepress/api/gh-icon/dpi/[...data].get.ts b/docs/.vitepress/api/gh-icon/dpi/[...data].get.ts index 2e5d4ebe4..5b2147dcb 100644 --- a/docs/.vitepress/api/gh-icon/dpi/[...data].get.ts +++ b/docs/.vitepress/api/gh-icon/dpi/[...data].get.ts @@ -1,45 +1,52 @@ import { eventHandler, setResponseHeader, defaultContentType } from 'h3'; -import { Resvg, initWasm } from '@resvg/resvg-wasm'; -import wasm from './loadWasm'; - -var initializedResvg = initWasm(wasm); +import iconNodes from '../../../data/iconNodes'; +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import createLucideIcon from 'lucide-react/src/createLucideIcon'; +import sharp from 'sharp'; export default eventHandler(async (event) => { const { params = {} } = event.context; - await initializedResvg; - const imageSize = 96; - const [iconSizeString, svgData] = params.data.split('/'); - const iconSize = parseInt(iconSizeString, 10); + const name = params.data.split('/').at(-3); + const iconSizeString = params.data.split('/').at(-2); + const svgData = params.data.split('/').at(-1); + const iconSize = Math.min(parseInt(iconSizeString, 10) || imageSize, 256); const data = svgData.slice(0, -4); + const prevSvg = iconNodes[name] + ? renderToStaticMarkup(createElement(createLucideIcon(name, iconNodes[name]))) + : undefined; + const src = Buffer.from(data, 'base64').toString('utf8'); const svg = (src.includes('${src}`) .replace(/(\r\n|\n|\r)/gm, '') .replace( - /]*/, + /]*>/, ` + ${prevSvg?.replaceAll('\n', '').replace(/]*>|<\/svg>/g, '')} + `, - ); + ) + .replace(/<\/svg>/, ''); - const resvg = new Resvg(svg, { background: '#000' }); - const pngData = resvg.render(); - const pngBuffer = Buffer.from(pngData.asPng()); + const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer(); defaultContentType(event, 'image/svg+xml'); setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000'); return ` - + `).children.map( - ({ name, attributes }) => [name, attributes], - ) as IconNode, - ); - - const svg = Buffer.from( - // We can't use jsx here, is not supported here by nitro. - renderToString(createElement(Icon, { strokeWidth })) - .replace(/fill\="none"/, 'fill="#fff"') - .replace( - />/, - `>`, - ), - ).toString('utf8'); - - defaultContentType(event, 'image/svg+xml'); - setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000'); - - return svg; -}); diff --git a/docs/.vitepress/api/gh-icon/stroke-width/[...data].get.tsx b/docs/.vitepress/api/gh-icon/stroke-width/[...data].get.tsx new file mode 100644 index 000000000..fed9410aa --- /dev/null +++ b/docs/.vitepress/api/gh-icon/stroke-width/[...data].get.tsx @@ -0,0 +1,37 @@ +import { eventHandler, setResponseHeader, defaultContentType } from 'h3'; +import { renderToString } from 'react-dom/server'; +import React from 'react'; +import Icon from 'lucide-react/src/Icon'; +import type { IconNode } from 'lucide-react/src/types'; +import { parseSync } from 'svgson'; + +export default eventHandler((event) => { + const { params } = event.context; + + const [strokeWidth, svgData] = params.data.split('/'); + const data = svgData.slice(0, -4); + + const src = Buffer.from(data, 'base64').toString('utf8'); + const iconNode = parseSync(src.includes('${src}`).children.map( + ({ name, attributes }) => [name, attributes], + ) as IconNode; + + defaultContentType(event, 'image/svg+xml'); + setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000'); + + return renderToString( + + + , + ); +}); diff --git a/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx b/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx new file mode 100644 index 000000000..08033bd4d --- /dev/null +++ b/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx @@ -0,0 +1,58 @@ +import { eventHandler, setResponseHeader, defaultContentType } from 'h3'; +import { renderToString } from 'react-dom/server'; +import React from 'react'; +import Diff from '../../../lib/SvgPreview/Diff.tsx'; + +export default eventHandler((event) => { + const { params } = event.context; + + const pathData = params.data.split('/'); + const data = pathData.at(-1).slice(0, -4); + const [operation] = pathData; + + const newSrc = Buffer.from(data, 'base64') + .toString('utf8') + .replaceAll('\n', '') + .replace(/]*>|<\/svg>/g, ''); + + const width = parseInt( + (newSrc.includes('${newSrc}`; + } else if (operation === 'flip-horizontal') { + oldSrc = `${newSrc}`; + } else if (operation === 'flip-vertical') { + oldSrc = `${newSrc}`; + } else if (operation === 'flip-backslash') { + oldSrc = `${newSrc}`; + } else if (operation === 'flip-slash') { + oldSrc = `${newSrc}`; + } else { + return ''; + } + + defaultContentType(event, 'image/svg+xml'); + setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000'); + + return renderToString( + + {children} + , + ); +}); diff --git a/docs/.vitepress/api/icons/[iconName].get.ts b/docs/.vitepress/api/icons/[iconName].get.ts index e2d2091bb..733eeb5ea 100644 --- a/docs/.vitepress/api/icons/[iconName].get.ts +++ b/docs/.vitepress/api/icons/[iconName].get.ts @@ -22,6 +22,7 @@ export default eventHandler((event) => { const height = getQuery(event).height || undefined; const color = getQuery(event).color || undefined; const strokeWidth = getQuery(event).strokeWidth || undefined; + const background = getQuery(event).background || undefined; const LucideIcon = createLucideIcon(params.iconName, iconNode); @@ -32,6 +33,7 @@ export default eventHandler((event) => { height, color: color ? `#${color}` : undefined, strokeWidth, + style: background ? { background } : undefined, }), ), ).toString('utf8'); diff --git a/docs/.vitepress/api/lab/icon-details/index.get.ts b/docs/.vitepress/api/lab/icon-details/index.get.ts new file mode 100644 index 000000000..c03ee6bc0 --- /dev/null +++ b/docs/.vitepress/api/lab/icon-details/index.get.ts @@ -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, + }, + ]; + }), + ); +}); diff --git a/docs/.vitepress/api/lab/icon-metadata/index.get.ts b/docs/.vitepress/api/lab/icon-metadata/index.get.ts new file mode 100644 index 000000000..84cb4e29c --- /dev/null +++ b/docs/.vitepress/api/lab/icon-metadata/index.get.ts @@ -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; +}); diff --git a/docs/.vitepress/api/lab/icon-nodes/index.get.ts b/docs/.vitepress/api/lab/icon-nodes/index.get.ts new file mode 100644 index 000000000..ac671867b --- /dev/null +++ b/docs/.vitepress/api/lab/icon-nodes/index.get.ts @@ -0,0 +1,30 @@ +import { eventHandler, getQuery, setResponseHeader } from 'h3'; +import iconNodes from '../../../data/lab/iconNodes'; +type IconNodeWithKeys = [elementName: string, attrs: Record & { 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; + }, {}); +}); diff --git a/docs/.vitepress/api/tags/index.get.ts b/docs/.vitepress/api/tags/index.get.ts index 2ffccbdc0..b2314c4f4 100644 --- a/docs/.vitepress/api/tags/index.get.ts +++ b/docs/.vitepress/api/tags/index.get.ts @@ -1,3 +1,4 @@ +import { eventHandler, setResponseHeader } from 'h3'; import iconMetaData from '../../data/iconMetaData'; export default eventHandler((event) => { diff --git a/docs/.vitepress/api/test.ts b/docs/.vitepress/api/test.ts deleted file mode 100644 index a3f2b67f3..000000000 --- a/docs/.vitepress/api/test.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default eventHandler(() => { - return { nitro: 'Is Awesome! asda' }; -}); diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8f75fa005..86ec55b0b 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,6 +1,19 @@ import { fileURLToPath, URL } from 'node:url'; -import { defineConfig } from 'vitepress'; +import { defineConfig, UserConfig } from 'vitepress'; +import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-icons'; import sidebar from './sidebar'; +import snackPlayer from './markdown/snackPlayer'; +import sandpackPlugin from './markdown/sandpack'; +import { readFile } from 'node:fs/promises'; +import { resourcesSidebar } from './sidebar/resources'; +import llmstxt from 'vitepress-plugin-llms'; +import { transformPageData } from './transformPageData'; +import getHeadConfig from './getHeadConfig'; + +const defaultSandpackCSS = await readFile( + fileURLToPath(new URL('./theme/sandpack-default.css', import.meta.url)), + 'utf-8', +); const title = 'Lucide'; const socialTitle = 'Lucide Icons'; @@ -9,10 +22,25 @@ const description = 'Beautiful & consistent icon toolkit made by the community.' // https://vitepress.dev/reference/site-config export default defineConfig({ title, + titleTemplate: ':title \u2013 Lucide', description, cleanUrls: true, outDir: '.vercel/output/static', srcExclude: ['**/README.md'], + markdown: { + config(md) { + md.use(groupIconMdPlugin); + md.use(snackPlayer); + md.use(sandpackPlugin, { + defaultFiles: { + '/styles.css': { + code: defaultSandpackCSS, + hidden: true, + }, + }, + }); + }, + }, vite: { resolve: { alias: [ @@ -28,123 +56,35 @@ export default defineConfig({ new URL('./theme/components/overrides/VPFooter.vue', import.meta.url), ), }, + { + find: /^.*\/VPCarbonAds\.vue$/, + replacement: fileURLToPath( + new URL('./theme/components/overrides/VPCarbonAds.vue', import.meta.url), + ), + }, { find: '~/.vitepress', replacement: fileURLToPath(new URL('./', import.meta.url)), }, ], }, + plugins: [ + groupIconVitePlugin(), + llmstxt({ + ignoreFiles: [ + 'code-of-conduct.md', + 'index.md', + 'packages.md', + 'showcase.md', + 'brand-logo-statement.md', + 'icons/**', // Not working, need investigation + ], + }) as unknown as UserConfig['vite']['plugins'][0], + ], }, - head: [ - [ - 'script', - { - src: 'https://analytics.lucide.dev/js/script.js', - 'data-domain': 'lucide.dev', - defer: '', - }, - ], - [ - 'meta', - { - property: 'og:locale', - content: 'en_US', - }, - ], - [ - 'meta', - { - property: 'og:type', - content: 'website', - }, - ], - [ - 'meta', - { - property: 'og:site_name', - content: title, - }, - ], - [ - 'meta', - { - property: 'og:title', - content: socialTitle, - }, - ], - [ - 'meta', - { - property: 'og:description', - content: description, - }, - ], - [ - 'meta', - { - property: 'og:url', - content: 'https://lucide.dev', - }, - ], - [ - 'meta', - { - property: 'og:image', - content: 'https://lucide.dev/og.png', - }, - ], - [ - 'meta', - { - property: 'og:image:width', - content: '1200', - }, - ], - [ - 'meta', - { - property: 'og:image:height', - content: '630', - }, - ], - [ - 'meta', - { - property: 'og:image:type', - content: 'image/png', - }, - ], - [ - 'meta', - { - property: 'twitter:card', - content: 'summary_large_image', - }, - ], - [ - 'meta', - { - property: 'twitter:title', - content: socialTitle, - }, - ], - [ - 'meta', - { - property: 'twitter:description', - content: description, - }, - ], - [ - 'meta', - { - property: 'twitter:image', - content: 'https://lucide.dev/og.png', - }, - ], - ], + head: getHeadConfig({ title, description, socialTitle }), + transformPageData, themeConfig: { - // https://vitepress.dev/reference/default-theme-config logo: { light: '/logo.light.svg', dark: '/logo.dark.svg', @@ -152,9 +92,16 @@ export default defineConfig({ nav: [ { text: 'Icons', link: '/icons/' }, { text: 'Guide', link: '/guide/' }, + { + text: 'Resources', + items: [ + ...resourcesSidebar[0].items, + { text: 'Design icons', link: '/contribute/icon-design-guide' }, + ], + }, { text: 'Packages', link: '/packages' }, { text: 'Showcase', link: '/showcase' }, - { text: 'License', link: '/license' }, + { text: 'Merch', link: 'https://merch.lucide.dev/' }, ], sidebar, socialLinks: [ @@ -163,7 +110,7 @@ export default defineConfig({ ], footer: { message: 'Released under the ISC License.', - copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`, + copyright: `Copyright © ${new Date().getFullYear()} Lucide Icons`, }, editLink: { pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path', diff --git a/docs/.vitepress/createOGImage.ts b/docs/.vitepress/createOGImage.ts new file mode 100644 index 000000000..84098314c --- /dev/null +++ b/docs/.vitepress/createOGImage.ts @@ -0,0 +1,79 @@ +import { PageData } from 'vitepress'; +import fs from 'node:fs/promises'; +import { cwd } from 'node:process'; +import { join } from 'node:path'; +import sharp from 'sharp'; + +const generalSVGTemplate = fs.readFile(join(cwd(), '.vitepress/lib/guideOGTemplate.svg'), 'utf-8'); + +export async function createGeneralOGImage({ title, description, relativePath }: PageData) { + const fileName = relativePath.replace(/\.md$/, '.png').replace(/\//g, '-'); + + const path = join(cwd(), 'public/og', fileName); + + let desc = description.replace(/&/g, '&').replace(//g, '>'); + if (!desc) { + desc = ''; + } + const lines = desc.replace(/(?![^\n]{1,64}$)([^\n]{1,64})\s/g, '$1\n').split('\n'); + const data = { + title: title, + line1: lines[0] || '', + line2: lines[1] || '', + line3: lines[2] || '', + line4: lines[3] || '', + }; + const svgTemplate = await generalSVGTemplate; + const svg = svgTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name: keyof typeof data) => data[name]); + + console.log(`Generating ${fileName}`); + try { + await sharp(Buffer.from(svg)) + .resize(1200 * 1.1, 630 * 1.1) + .png() + .toFile(path); + } catch (e) { + console.error('Error generating', { filename: path, ...data, svg }); + console.error(e); + } + + return `/og/${fileName}`; +} + +const iconSVGTemplate = fs.readFile(join(cwd(), '.vitepress/lib/iconOGTemplate.svg'), 'utf-8'); + +export async function createIconOGImage(iconName: string, tags: string[]) { + const path = join(cwd(), 'public/og', `icon-${iconName}.png`); + + let iconSVG = await fs.readFile(join(cwd(), `../icons/${iconName}.svg`), 'utf-8'); + iconSVG = iconSVG + .replace('width="24"', 'width="320"') + .replace('height="24"', 'height="320"') + .replace('currentColor', '#dfdfd6') + .replace( + 'xmlns="http://www.w3.org/2000/svg"', + 'xmlns="http://www.w3.org/2000/svg" x="62" y="66"', + ); + + const data = { + name: iconName, + tags: tags.join(' • ').replace(//g, '>').replace(/&/g, '&'), + iconSVG, + }; + + const svgTemplate = await iconSVGTemplate; + const svg = svgTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name: keyof typeof data) => data[name]); + + console.log(`Generating ${path}`); + try { + await sharp(Buffer.from(svg)) + .resize(1200 * 1.1, 630 * 1.1) + .png() + .toFile(path); + } catch (e) { + console.error('Error generating', { filename: path, ...data, svg }); + console.error(e); + } + + return `/og/icon-${iconName}.png`; +} diff --git a/docs/.vitepress/data/categoriesData.json b/docs/.vitepress/data/categoriesData.json index 03712cc23..dcb8e5f07 100644 --- a/docs/.vitepress/data/categoriesData.json +++ b/docs/.vitepress/data/categoriesData.json @@ -15,10 +15,6 @@ "name": "arrows", "title": "Arrows" }, - { - "name": "brands", - "title": "Brands" - }, { "name": "buildings", "title": "Buildings" @@ -35,10 +31,6 @@ "name": "connectivity", "title": "Connectivity" }, - { - "name": "currency", - "title": "Currency" - }, { "name": "cursors", "title": "Cursors" @@ -64,12 +56,12 @@ "title": "File icons" }, { - "name": "food-beverage", - "title": "Food & beverage" + "name": "finance", + "title": "Finance" }, { - "name": "furniture", - "title": "Furniture" + "name": "food-beverage", + "title": "Food & beverage" }, { "name": "gaming", @@ -88,21 +80,13 @@ "title": "Mail" }, { - "name": "maps", - "title": "Maps" - }, - { - "name": "maths", - "title": "Maths" + "name": "math", + "title": "Mathematics" }, { "name": "medical", "title": "Medical" }, - { - "name": "money", - "title": "Money" - }, { "name": "multimedia", "title": "Multimedia" @@ -113,11 +97,11 @@ }, { "name": "navigation", - "title": "Navigation" + "title": "Navigation & Places" }, { "name": "notifications", - "title": "Notifications" + "title": "Notification" }, { "name": "people", diff --git a/docs/.vitepress/data/companiesData.json b/docs/.vitepress/data/companiesData.json index 8e1439ad7..c0f71a7c1 100644 --- a/docs/.vitepress/data/companiesData.json +++ b/docs/.vitepress/data/companiesData.json @@ -7,6 +7,14 @@ "dark": "/company-logos/vercel-dark.svg" } }, + { + "name": "MDN Web Docs", + "url": "https://developer.mozilla.org/", + "image": { + "light": "/company-logos/mdn-light.svg", + "dark": "/company-logos/mdn-dark.svg" + } + }, { "name": "Supabase", "url": "https://supabase.com", @@ -23,6 +31,14 @@ "dark": "/company-logos/obsidian-dark.svg" } }, + { + "name": "Nuxt", + "url": "https://nuxt.com/", + "image": { + "light": "/company-logos/nuxt-light.svg", + "dark": "/company-logos/nuxt-dark.svg" + } + }, { "name": "Open Collective", "url": "https://opencollective.com", @@ -30,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" - } } ] diff --git a/docs/.vitepress/data/componentLibrariesData.json b/docs/.vitepress/data/componentLibrariesData.json index c0534b9aa..bac1d2ad1 100644 --- a/docs/.vitepress/data/componentLibrariesData.json +++ b/docs/.vitepress/data/componentLibrariesData.json @@ -14,5 +14,13 @@ "light": "/library-logos/tamagui.svg", "dark": "/library-logos/tamagui.svg" } + }, + { + "name": "Reflex", + "url": "https://reflex.dev", + "image": { + "light": "/library-logos/reflex-light.svg", + "dark": "/library-logos/reflex-dark.svg" + } } ] diff --git a/docs/.vitepress/data/lab/.gitkeep b/docs/.vitepress/data/lab/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/docs/.vitepress/data/packageData.json b/docs/.vitepress/data/packageData.json index 613ed60e7..9dd193f59 100644 --- a/docs/.vitepress/data/packageData.json +++ b/docs/.vitepress/data/packageData.json @@ -18,6 +18,7 @@ "lucide-react": { "order": 1, "icon": "react", + "docsAlias": "react", "shields": [ { "alt": "npm", @@ -31,57 +32,46 @@ } ] }, - "lucide-vue": { + "@lucide/vue": { "order": 2, "icon": "vue", + "docsAlias": "vue", + "packageDirname": "vue", "shields": [ { "alt": "npm", - "src": "https://img.shields.io/npm/v/lucide-vue", - "href": "https://www.npmjs.com/package/lucide-vue" + "src": "https://img.shields.io/npm/v/@lucide/vue", + "href": "https://www.npmjs.com/package/@lucide/vue" }, { "alt": "npm", - "src": "https://img.shields.io/npm/dw/lucide-vue", - "href": "https://www.npmjs.com/package/lucide-vue" + "src": "https://img.shields.io/npm/dw/@lucide/vue", + "href": "https://www.npmjs.com/package/@lucide/vue" } ] }, - "lucide-vue-next": { + "@lucide/svelte": { "order": 3, - "icon": "vue-next", - "shields": [ - { - "alt": "npm", - "src": "https://img.shields.io/npm/v/lucide-vue-next", - "href": "https://www.npmjs.com/package/lucide-vue-next" - }, - { - "alt": "npm", - "src": "https://img.shields.io/npm/dw/lucide-vue-next", - "href": "https://www.npmjs.com/package/lucide-vue-next" - } - ] - }, - "lucide-svelte": { - "order": 4, "icon": "svelte", + "docsAlias": "svelte", + "packageDirname": "svelte", "shields": [ { "alt": "npm", - "src": "https://img.shields.io/npm/v/lucide-svelte", - "href": "https://www.npmjs.com/package/lucide-svelte" + "src": "https://img.shields.io/npm/v/@lucide/svelte", + "href": "https://www.npmjs.com/package/@lucide/svelte" }, { "alt": "npm", - "src": "https://img.shields.io/npm/dw/lucide-svelte", - "href": "https://www.npmjs.com/package/lucide-svelte" + "src": "https://img.shields.io/npm/dw/@lucide/svelte", + "href": "https://www.npmjs.com/package/@lucide/svelte" } ] }, "lucide-solid": { "order": 4, "icon": "solid", + "docsAlias": "solid", "shields": [ { "alt": "npm", @@ -95,25 +85,10 @@ } ] }, - "lucide-preact": { - "order": 5, - "icon": "preact", - "shields": [ - { - "alt": "npm", - "src": "https://img.shields.io/npm/v/lucide-preact", - "href": "https://www.npmjs.com/package/lucide-preact" - }, - { - "alt": "npm", - "src": "https://img.shields.io/npm/dw/lucide-preact", - "href": "https://www.npmjs.com/package/lucide-preact" - } - ] - }, "lucide-react-native": { - "order": 6, + "order": 5, "icon": "react-native", + "docsAlias": "react-native", "shields": [ { "alt": "npm", @@ -127,25 +102,64 @@ } ] }, - "lucide-angular": { - "order": 7, + "@lucide/angular": { + "order": 6, "icon": "angular", + "docsAlias": "angular", + "packageDirname": "angular", "shields": [ { "alt": "npm", - "src": "https://img.shields.io/npm/v/lucide-angular", - "href": "https://www.npmjs.com/package/lucide-angular" + "src": "https://img.shields.io/npm/v/@lucide/angular", + "href": "https://www.npmjs.com/package/@lucide/angular" }, { "alt": "npm", - "src": "https://img.shields.io/npm/dw/lucide-angular", - "href": "https://www.npmjs.com/package/lucide-angular" + "src": "https://img.shields.io/npm/dw/@lucide/angular", + "href": "https://www.npmjs.com/package/@lucide/angular" + } + ] + }, + "lucide-preact": { + "order": 8, + "icon": "preact", + "docsAlias": "preact", + "shields": [ + { + "alt": "npm", + "src": "https://img.shields.io/npm/v/lucide-preact", + "href": "https://www.npmjs.com/package/lucide-preact" + }, + { + "alt": "npm", + "src": "https://img.shields.io/npm/dw/lucide-preact", + "href": "https://www.npmjs.com/package/lucide-preact" + } + ] + }, + "@lucide/astro": { + "docsAlias": "astro", + "packageDirname": "astro", + "order": 9, + "icon": "astro", + "iconDark": "astro-dark", + "shields": [ + { + "alt": "npm", + "src": "https://img.shields.io/npm/v/@lucide/astro", + "href": "https://www.npmjs.com/package/@lucide/astro" + }, + { + "alt": "npm", + "src": "https://img.shields.io/npm/dw/@lucide/astro", + "href": "https://www.npmjs.com/package/@lucide/astro" } ] }, "lucide-static": { - "order": 8, + "order": 10, "icon": "svg", + "docsAlias": "static", "shields": [ { "alt": "npm", @@ -158,5 +172,24 @@ "href": "https://www.npmjs.com/package/lucide-static" } ] + }, + "@lucide/icons": { + "hide": true, + "docsAlias": "icons", + "packageDirname": "icons", + "order": 10, + "icon": "ts", + "shields": [ + { + "alt": "npm", + "src": "https://img.shields.io/npm/v/@lucide/icons", + "href": "https://www.npmjs.com/package/@lucide/icons" + }, + { + "alt": "npm", + "src": "https://img.shields.io/npm/dw/@lucide/icons", + "href": "https://www.npmjs.com/package/@lucide/icons" + } + ] } -} +} \ No newline at end of file diff --git a/docs/.vitepress/data/packageData.thirdParty.json b/docs/.vitepress/data/packageData.thirdParty.json index ba35181a5..37f44cad6 100644 --- a/docs/.vitepress/data/packageData.thirdParty.json +++ b/docs/.vitepress/data/packageData.thirdParty.json @@ -76,5 +76,157 @@ ], "source": "https://github.com/swisnl/nuxt-lucide-icons", "documentation": "https://github.com/swisnl/nuxt-lucide-icons/blob/main/README.md" + }, + { + "name": "lucide_lustre", + "description": "A library providing https://lucide.dev icons to lustre.", + "icon": "/framework-logos/lustre.webp", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/hexpm/v/lucide_lustre", + "href": "https://hex.pm/packages/lucide_lustre" + }, + { + "alt": "Total Downloads", + "src": "https://img.shields.io/hexpm/dw/lucide_lustre", + "href": "https://hex.pm/packages/lucide_lustre" + } + ], + "source": "https://github.com/dinkelspiel/lucide_lustre", + "documentation": "https://github.com/dinkelspiel/lucide_lustre/blob/master/README.md" + }, + { + "name": "lucide_icons_flutter", + "description": "A library providing https://lucide.dev icons to Flutter.", + "icon": "/framework-logos/flutter.svg", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/pub/v/lucide_icons_flutter", + "href": "https://pub.dev/packages/lucide_icons_flutter" + }, + { + "alt": "Total Downloads", + "src": "https://img.shields.io/pub/dm/lucide_icons_flutter", + "href": "https://pub.dev/packages/lucide_icons_flutter" + } + ], + "source": "https://github.com/vqh2602/lucide-flutter-main", + "documentation": "https://pub.dev/documentation/lucide_icons_flutter/latest/" + }, + { + "name": "lucide-slint", + "description": "Implementation of the Lucide icon library for Slint.", + "icon": "/framework-logos/slint.svg", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/crates/v/lucide-slint", + "href": "https://crates.io/crates/lucide-slint" + }, + { + "alt": "Total Downloads", + "src": "https://img.shields.io/crates/d/lucide-slint", + "href": "https://crates.io/crates/lucide-slint" + } + ], + "source": "https://github.com/cnlancehu/lucide-slint", + "documentation": "https://github.com/cnlancehu/lucide-slint/blob/main/README.md" + }, + { + "name": "lucide-go", + "description": "Implementation of Lucide icons for Go's html/template package.", + "icon": "/framework-logos/go.svg", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/github/v/release/kaugesaar/lucide-go", + "href": "https://github.com/kaugesaar/lucide-go/releases" + }, + { + "alt": "Go Reference", + "src": "https://pkg.go.dev/badge/github.com/kaugesaar/lucide-go.svg", + "href": "https://pkg.go.dev/github.com/kaugesaar/lucide-go" + } + ], + "source": "https://github.com/kaugesaar/lucide-go", + "documentation": "https://github.com/kaugesaar/lucide-go/blob/master/README.md" + }, + { + "name": "lucide-rails", + "description": "Ruby on Rails views helper method for rendering Lucide icons.", + "icon": "/framework-logos/rails.svg", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/gem/v/lucide-rails", + "href": "https://rubygems.org/gems/lucide-rails" + }, + { + "alt": "Total Downloads", + "src": "https://img.shields.io/gem/dt/lucide-rails", + "href": "https://rubygems.org/gems/lucide-rails" + } + ], + "source": "https://github.com/heyvito/lucide-rails", + "documentation": "https://github.com/heyvito/lucide-rails/blob/master/README.md" + }, + { + "name": "lucide-web-components", + "description": "Implementation of Lucide icons in Web Components custom elements.", + "icon": "/framework-logos/web-components.svg", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/npm/v/@dile/iconlib", + "href": "https://dile-components.com/icons/lucide-icons/" + }, + { + "alt": "Total Downloads", + "src": "https://img.shields.io/npm/dt/@dile/iconlib", + "href": "https://dile-components.com/icons/lucide-icons/" + } + ], + "source": "https://github.com/Polydile/dile-components", + "documentation": "https://github.com/Polydile/dile-components/blob/master/packages/iconlib/README.md" + }, + { + "name": "strapi-lucide-icons", + "description": "Lucide icon picker custom field for Strapi.", + "icon": "/framework-logos/strapi.svg", + "shields": [ + { + "alt": "Latest Stable Version", + "src": "https://img.shields.io/npm/v/strapi-lucide-icons", + "href": "https://www.npmjs.com/package/strapi-lucide-icons" + }, + { + "alt": "Total Downloads", + "src": "https://img.shields.io/npm/dw/strapi-lucide-icons", + "href": "https://www.npmjs.com/package/strapi-lucide-icons" + } + ], + "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" } ] diff --git a/docs/.vitepress/data/teamData.json b/docs/.vitepress/data/teamData.json index 3e5504aee..4f46f57f1 100644 --- a/docs/.vitepress/data/teamData.json +++ b/docs/.vitepress/data/teamData.json @@ -11,7 +11,7 @@ }, { "icon": "x", - "link": "https://github.com/ericfennis" + "link": "https://x.com/ericfennis" } ] }, diff --git a/docs/.vitepress/getHeadConfig.ts b/docs/.vitepress/getHeadConfig.ts new file mode 100644 index 000000000..95ebe50f0 --- /dev/null +++ b/docs/.vitepress/getHeadConfig.ts @@ -0,0 +1,127 @@ +import { HeadConfig } from 'vitepress'; + +const getHeadConfig = ({ + title, + description, + socialTitle, +}: { + title: string; + description: string; + socialTitle?: string; +}): HeadConfig[] => [ + [ + 'link', + { + rel: 'preconnect', + href: 'https://analytics.lucide.dev', + }, + ], + [ + 'script', + { + src: 'https://analytics.lucide.dev/js/script.js', + 'data-domain': 'lucide.dev', + defer: '', + }, + ], + [ + 'meta', + { + property: 'og:locale', + content: 'en_US', + }, + ], + [ + 'meta', + { + property: 'og:type', + content: 'website', + }, + ], + [ + 'meta', + { + property: 'og:site_name', + content: title, + }, + ], + [ + 'meta', + { + property: 'og:title', + content: socialTitle, + }, + ], + [ + 'meta', + { + property: 'og:description', + content: description, + }, + ], + [ + 'meta', + { + property: 'og:url', + content: 'https://lucide.dev', + }, + ], + [ + 'meta', + { + property: 'og:image', + content: 'https://lucide.dev/og.png', + }, + ], + [ + 'meta', + { + property: 'og:image:width', + content: '1200', + }, + ], + [ + 'meta', + { + property: 'og:image:height', + content: '630', + }, + ], + [ + 'meta', + { + property: 'og:image:type', + content: 'image/png', + }, + ], + [ + 'meta', + { + property: 'twitter:card', + content: 'summary_large_image', + }, + ], + [ + 'meta', + { + property: 'twitter:title', + content: socialTitle, + }, + ], + [ + 'meta', + { + property: 'twitter:description', + content: description, + }, + ], + [ + 'meta', + { + property: 'twitter:image', + content: 'https://lucide.dev/og.png', + }, + ], +]; + +export default getHeadConfig; diff --git a/docs/.vitepress/getStructuredData.ts b/docs/.vitepress/getStructuredData.ts new file mode 100644 index 000000000..2ac47df1e --- /dev/null +++ b/docs/.vitepress/getStructuredData.ts @@ -0,0 +1,46 @@ +import { PageData } from 'vitepress'; + +export default async function getStructuredData(iconName: string, pageData: PageData) { + const url = `https://lucide.dev/icons/${iconName}`; + + return { + '@context': 'https://schema.org', + '@type': 'WebPage', + '@id': url, + name: pageData.title, + description: pageData.description, + url, + inLanguage: 'en', + isPartOf: { + '@type': 'WebSite', + name: 'Lucide Icons', + url: 'https://lucide.dev', + }, + breadcrumb: { + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Icons', item: 'https://lucide.dev/icons' }, + { '@type': 'ListItem', position: 2, name: iconName, item: url }, + ], + }, + mainEntity: { + '@type': 'ImageObject', + '@id': `${url}#icon`, + name: iconName, + // TODO: Add support for description extraction from icon metadata + // description: 'An ...SVG icon from the Lucide icon set.', + contentUrl: `https://lucide.dev/api/icons/${iconName}}?width=24&height=24&background=white`, + thumbnailUrl: `https://lucide.dev/api/icons/${iconName}}?width=256&height=256&background=white`, + encodingFormat: 'image/svg+xml', + keywords: pageData.params?.tags, + width: 24, + height: 24, + creator: { + '@type': 'Organization', + name: 'Lucide Icons', + }, + datePublished: pageData?.params?.createdRelease?.date, + dateModified: pageData?.params?.changedRelease?.date, + }, + }; +} diff --git a/docs/.vitepress/lib/SvgPreview/Backdrop.tsx b/docs/.vitepress/lib/SvgPreview/Backdrop.tsx index 088dcf7e9..05d43f68d 100644 --- a/docs/.vitepress/lib/SvgPreview/Backdrop.tsx +++ b/docs/.vitepress/lib/SvgPreview/Backdrop.tsx @@ -3,10 +3,16 @@ import React from 'react'; interface BackdropProps { src: string; color?: string; + outline?: boolean; backdropString: string; } -const Backdrop = ({ src, color = 'red', backdropString }: BackdropProps): JSX.Element => { +const Backdrop = ({ + src, + color = 'red', + outline = true, + backdropString, +}: BackdropProps): JSX.Element => { const id = React.useId(); return ( <> @@ -47,8 +53,8 @@ const Backdrop = ({ src, color = 'red', backdropString }: BackdropProps): JSX.El @@ -61,8 +67,8 @@ const Backdrop = ({ src, color = 'red', backdropString }: BackdropProps): JSX.El - + {outline && ( + + )} ); diff --git a/docs/.vitepress/lib/SvgPreview/Diff.tsx b/docs/.vitepress/lib/SvgPreview/Diff.tsx new file mode 100644 index 000000000..dfe39dec6 --- /dev/null +++ b/docs/.vitepress/lib/SvgPreview/Diff.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import Backdrop from './Backdrop.tsx'; +import { darkModeCss, Grid } from './index.tsx'; + +const SvgPreview = React.forwardRef< + SVGSVGElement, + { + oldSrc: string; + newSrc: string; + height: number; + width: number; + } & React.SVGProps +>(({ oldSrc, newSrc, children, height, width, ...props }, ref) => { + return ( + + + + + + + + ${newSrc}`} + color="#777" + /> + + + {children} + + ); +}); + +export default SvgPreview; diff --git a/docs/.vitepress/lib/SvgPreview/GapViolationHighlight.tsx b/docs/.vitepress/lib/SvgPreview/GapViolationHighlight.tsx new file mode 100644 index 000000000..89c286e08 --- /dev/null +++ b/docs/.vitepress/lib/SvgPreview/GapViolationHighlight.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import pathToPoints from './path-to-points'; +import { Path, PathProps } from './types'; + +export const GapViolationHighlight = ({ + stroke, + strokeWidth, + strokeOpacity, + paths, + ...props +}: { + paths: Path[]; +} & PathProps<'stroke' | 'strokeOpacity' | 'strokeWidth', 'd'>) => { + const id = React.useId(); + + const groupedPaths = Object.entries( + paths.reduce( + (groups, val) => { + const key = val.c.id; + groups[key] = [...(groups[key] || []), val]; + return groups; + }, + {} as Record, + ), + ); + + const groups: Group[] = []; + + for (const [, paths] of groupedPaths) { + const d = paths.map((path) => path.d).join(' '); + const points = paths.flatMap((path) => pathToPoints(path)); + groups.push({ id: d, points }); + } + + const mergedGroups = mergeGroups(groups, 2); + + return ( + + + + + + + + {mergedGroups.flatMap((ds, idx, arr) => + arr.slice(0, idx).map((val, i) => ( + + + + + + + )), + )} + + ); +}; + +type Point = { x: number; y: number }; +type Group = { id: string; points: Point[] }; + +// Euclidean distance +function distance(a: Point, b: Point): number { + return Math.hypot(a.x - b.x, a.y - b.y); +} + +// Check if two groups should be merged based on minimum distance +function shouldMerge(a: Group, b: Group, minDistance: number): boolean { + for (const pa of a.points) { + for (const pb of b.points) { + if (distance(pa, pb) <= minDistance) { + return true; + } + } + } + return false; +} + +// Merge groups and return arrays of merged group IDs +function mergeGroups(groups: Group[], minDistance: number): string[][] { + const mergedGroups: Group[][] = groups.map((g) => [g]); + + let changed = true; + while (changed) { + changed = false; + + outer: for (let i = 0; i < mergedGroups.length; i++) { + for (let j = i + 1; j < mergedGroups.length; j++) { + // Check if any group in mergedGroups[i] should merge with any in mergedGroups[j] + if ( + mergedGroups[i].some((ga) => + mergedGroups[j].some((gb) => shouldMerge(ga, gb, minDistance)), + ) + ) { + // Merge group j into group i + mergedGroups[i] = [...mergedGroups[i], ...mergedGroups[j]]; + mergedGroups.splice(j, 1); + changed = true; + break outer; + } + } + } + } + + // Return only arrays of IDs + return mergedGroups.map((groupList) => groupList.map((g) => g.id)); +} diff --git a/docs/.vitepress/lib/SvgPreview/index.tsx b/docs/.vitepress/lib/SvgPreview/index.tsx index 5d6b5d386..60643fa87 100644 --- a/docs/.vitepress/lib/SvgPreview/index.tsx +++ b/docs/.vitepress/lib/SvgPreview/index.tsx @@ -1,13 +1,36 @@ import React from 'react'; import { PathProps, Path } from './types'; -import { getPaths, assert } from './utils'; +import getPaths, { assert } from './utils'; +import { GapViolationHighlight } from './GapViolationHighlight.tsx'; -const Grid = ({ +export const darkModeCss = ` + @media screen and (prefers-color-scheme: light) { + .svg-preview-grid-rect { fill: none } + } + @media screen and (prefers-color-scheme: dark) { + .svg-preview-grid-rect { fill: none } + .svg + .svg-preview-grid-group, + .svg-preview-radii-group, + .svg-preview-shadow-mask-group, + .svg-preview-shadow-group { + stroke: #fff; + } + } +`; + +export const Grid = ({ radius, - fill = '#fff', + fill, + height, + width, + subGridSize = 0, ...props }: { + height: number; + width: number; strokeWidth: number; + subGridSize?: number; radius: number; } & PathProps<'stroke', 'strokeWidth'>) => ( i) - .filter((i) => i % 3 !== 2) - .flatMap((i) => [ - `M${props.strokeWidth} ${i + 1}h${24 - props.strokeWidth * 2}`, - `M${i + 1} ${props.strokeWidth}v${24 - props.strokeWidth * 2}`, - ]) - .join('') - } - /> - i) - .filter((i) => i % 3 === 2) - .flatMap((i) => [ - `M${props.strokeWidth} ${i + 1}h${24 - props.strokeWidth * 2}`, - `M${i + 1} ${props.strokeWidth}v${24 - props.strokeWidth * 2}`, - ]) - .join('') + [ + ...new Array(Math.floor(width - 1)) + .fill(null) + .map((_, i) => i) + .filter((i) => !subGridSize || i % subGridSize !== subGridSize - 1) + .flatMap((i) => [`M${i + 1} ${props.strokeWidth}v${height - props.strokeWidth * 2}`]), + ...new Array(Math.floor(height - 1)) + .fill(null) + .map((_, i) => i) + .filter((i) => !subGridSize || i % subGridSize !== subGridSize - 1) + .flatMap((i) => [`M${props.strokeWidth} ${i + 1}h${width - props.strokeWidth * 2}`]), + ].join('') } /> + {!!subGridSize && ( + i) + .filter((i) => i % subGridSize === subGridSize - 1) + .flatMap((i) => [`M${i + 1} ${props.strokeWidth}v${height - props.strokeWidth * 2}`]), + ...new Array(Math.floor(height - 1)) + .fill(null) + .map((_, i) => i) + .filter((i) => i % subGridSize === subGridSize - 1) + .flatMap((i) => [`M${props.strokeWidth} ${i + 1}h${width - props.strokeWidth * 2}`]), + ].join('') + } + /> + )} ); @@ -83,6 +116,7 @@ const Shadow = ({ > {groupedPaths.map(([id, paths]) => ( ) => ( - - {paths.map(({ d, c }, i) => ( - - ))} - -); +}: { paths: Path[]; colors: string[] } & PathProps) => { + let idx = 0; + return ( + + {paths.map(({ d, c }, i) => ( + + ))} + + ); +}; const ControlPath = ({ paths, radius, pointSize, ...props -}: { pointSize: number; paths: Path[]; radius: number } & PathProps< - 'stroke' | 'strokeWidth', - 'd' ->) => { +}: { + pointSize: number; + paths: Path[]; + radius: number; +} & PathProps<'stroke' | 'strokeWidth', 'd'>) => { const controlPaths = paths.map((path, i) => { const element = paths.filter((p) => p.c.id === path.c.id); const lastElement = element.at(-1)?.next; @@ -191,8 +229,8 @@ const ControlPath = ({ - {controlPaths.map(({ d, prev, next, startMarker, endMarker }, i) => ( + {controlPaths.map(({ prev, next, startMarker, endMarker }, i) => ( {startMarker && ( ) => { return ( {paths.map( - ({ c, prev, next, circle }, i) => + ({ circle, next, prev, c }, i) => circle && ( - {c.name !== 'circle' && ( - + {circle.tangentIntersection && c.name === 'path' && ( + <> + + + + + + )} + {c.name === 'path' && ( + )} ) => { - return ( - - {paths.map(({ c, prev, next, cp1, cp2 }) => ( - <> - {cp1 && } - {cp1 && ( - - )} - {cp2 && } - {cp2 && ( - - )} - - ))} - - ); -}; +}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', never>) => ( + + {paths.map(({ prev, next, cp1, cp2 }, i) => ( + + {cp1 && } + {cp1 && ( + + )} + {cp2 && } + {cp2 && ( + + )} + + ))} + +); -const SvgPreview = React.forwardRef< - SVGSVGElement, - { - src: string | ReturnType; - showGrid?: boolean; - } & React.SVGProps ->(({ src, children, showGrid = false, ...props }, ref) => { +interface SvgPreviewProps extends React.SVGProps { + height?: number; + width?: number; + src: string | ReturnType; + showGrid?: boolean; + children?: React.ReactNode; +} + +const SvgPreview = ( + { src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps, + ref, +) => { + const subGridSize = + Math.max(height, width) % 3 === 0 + ? Math.max(height, width) > 24 + ? 12 + : 3 + : Math.max(height, width) % 5 === 0 + ? 5 + : 0; const paths = typeof src === 'string' ? getPaths(src) : src; - const darkModeCss = `@media screen and (prefers-color-scheme: light) { - .svg-preview-grid-rect { fill: none } -} -@media screen and (prefers-color-scheme: dark) { - .svg-preview-grid-rect { fill: none } - .svg - .svg-preview-grid-group, - .svg-preview-radii-group, - .svg-preview-shadow-mask-group, - .svg-preview-shadow-group { - stroke: #fff; - } -}`; return ( {darkModeCss} {showGrid && ( @@ -382,6 +445,12 @@ const SvgPreview = React.forwardRef< radius={1} strokeOpacity={0.15} /> + ); -}); +}; + +SvgPreview.displayName = 'SvgPreview'; export default SvgPreview; diff --git a/docs/.vitepress/lib/SvgPreview/path-to-points.ts b/docs/.vitepress/lib/SvgPreview/path-to-points.ts new file mode 100644 index 000000000..8f3f64f06 --- /dev/null +++ b/docs/.vitepress/lib/SvgPreview/path-to-points.ts @@ -0,0 +1,21 @@ +import { memoize } from 'lodash-es'; +import SVGPathCommander from 'svg-path-commander'; +import { Path } from './types'; + +function pathToPoints({ d, prev, next }: Path, interval = 1) { + const commander = new SVGPathCommander(d); + const points = []; + try { + const totalLength = commander.getTotalLength(); + points.push(prev); + for (let i = interval; i < totalLength - interval; i += interval) { + points.push(commander.getPointAtLength(i)); + } + points.push(next); + } catch (err) { + console.warn(`Failed to convert path to points for d="${d}": ${err}`); + } + return points; +} + +export default memoize(pathToPoints); diff --git a/docs/.vitepress/lib/SvgPreview/types.ts b/docs/.vitepress/lib/SvgPreview/types.ts index 8c1b8f4ed..ecfe201db 100644 --- a/docs/.vitepress/lib/SvgPreview/types.ts +++ b/docs/.vitepress/lib/SvgPreview/types.ts @@ -8,7 +8,7 @@ export type Path = { prev: Point; next: Point; isStart: boolean; - circle?: { x: number; y: number; r: number }; + circle?: { x: number; y: number; r: number; tangentIntersection?: Point }; cp1?: Point; cp2?: Point; c: ReturnType[number]; diff --git a/docs/.vitepress/lib/SvgPreview/utils.ts b/docs/.vitepress/lib/SvgPreview/utils.ts index 9dfb1a6f7..04ba47f5c 100644 --- a/docs/.vitepress/lib/SvgPreview/utils.ts +++ b/docs/.vitepress/lib/SvgPreview/utils.ts @@ -2,6 +2,7 @@ import { INode, parseSync } from 'svgson'; import toPath from 'element-to-path'; import { SVGPathData, encodeSVGPath } from 'svg-pathdata'; import { Path, Point } from './types'; +import { memoize } from 'lodash-es'; function assertNever(x: never): never { throw new Error('Unknown type: ' + x['type']); @@ -44,17 +45,21 @@ const extractNodes = (node: INode): INode[] => { return []; }; -export const getNodes = (src: string) => - extractNodes(parseSync(src.includes('${src}`)); +export const getNodes = memoize((src: string) => + extractNodes(parseSync(src.includes('${src}`)), +); export const getCommands = (src: string) => getNodes(src) .map(convertToPathNode) .flatMap(({ d, name }, idx) => - new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })), + new SVGPathData(d) + .toAbs() + // @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 })), ); -export const getPaths = (src: string) => { +const getPaths = (src: string) => { const commands = getCommands(src.includes('${src}`); const paths: Path[] = []; let prev: Point | undefined = undefined; @@ -237,6 +242,7 @@ export const getPaths = (src: string) => { break; } default: { + // @ts-expect-error every command type is handled above, so `c` is not `never` here assertNever(c); } } @@ -244,7 +250,7 @@ export const getPaths = (src: string) => { return paths; }; -export const arcEllipseCenter = ( +const arcEllipseCenter = ( x1: number, y1: number, rx: number, @@ -296,5 +302,52 @@ export const arcEllipseCenter = ( M2[1][0] * Cp[0] + M2[1][1] * Cp[1] + V3[1], ]; - return { x: C[0], y: C[1] }; + return { + x: C[0], + y: C[1], + tangentIntersection: intersectTangents( + { x: x1, y: y1 }, + { x: x2, y: y2 }, + { x: C[0], y: C[1] }, + ), + }; }; + +function getTangentDirection(p: Point, center: Point): Point { + // Tangent is perpendicular to the radius vector (rotate radius 90°) + const dx = p.x - center.x; + const dy = p.y - center.y; + return { x: -dy, y: dx }; // 90° rotation +} + +function intersectTangents(start: Point, end: Point, center: Point): Point | null { + const t1 = getTangentDirection(start, center); + const t2 = getTangentDirection(end, center); + + // Solve: start + λ * t1 = end + μ * t2 + const A = [ + [t1.x, -t2.x], + [t1.y, -t2.y], + ]; + const b = [end.x - start.x, end.y - start.y]; + + // Compute determinant + const det = A[0][0] * A[1][1] - A[0][1] * A[1][0]; + + if (Math.abs(det) < 1e-10) { + // Lines are parallel, no intersection + return null; + } + + const invDet = 1 / det; + + const lambda = (b[0] * A[1][1] - b[1] * A[0][1]) * invDet; + + // Intersection point = start + lambda * t1 + return { + x: start.x + lambda * t1.x, + y: start.y + lambda * t1.y, + }; +} + +export default memoize(getPaths); diff --git a/docs/.vitepress/lib/codeExamples/createCodeExamples.ts b/docs/.vitepress/lib/codeExamples/createCodeExamples.ts index 7848841c2..ceafb02ca 100644 --- a/docs/.vitepress/lib/codeExamples/createCodeExamples.ts +++ b/docs/.vitepress/lib/codeExamples/createCodeExamples.ts @@ -1,5 +1,4 @@ -import { bundledLanguages, type ThemeRegistration } from 'shikiji'; -import { getHighlighter } from 'shikiji'; +import { bundledLanguages, createHighlighter, type ThemeRegistration } from 'shiki'; type CodeExampleType = { title: string; @@ -10,14 +9,20 @@ type CodeExampleType = { const getIconCodes = (): CodeExampleType => { return [ { - language: 'js', + language: 'html', title: 'Vanilla', code: `\ -import { createIcons, icons } from 'lucide'; + -document.body.append('');\ +\ `, }, { @@ -38,7 +43,7 @@ export default App; language: 'vue', title: 'Vue', code: `