Compare commits

..
Author SHA1 Message Date
Eric Fennis 457a2d1766 Add robots and theme color 2026-03-26 13:46:39 +01:00
Eric Fennis 8679e10aaa add head config 2026-03-26 12:05:22 +01:00
Eric Fennis e1ee65bc23 Fix site SEO for icon image 2026-03-26 11:53:34 +01:00
2052 changed files with 26921 additions and 18260 deletions
+11 -9
View File
@@ -13,19 +13,21 @@ body:
description: Which Lucide packages are affected? You may select more than one.
options:
- label: lucide
- label: lucide-react
- label: '@lucide/vue'
- label: '@lucide/svelte'
- label: '@lucide/angular'
- label: lucide-angular (old version)
- label: '@lucide/angular (new version)'
- label: '@lucide/astro'
- label: lucide-react-native
- label: lucide-flutter
- label: lucide-preact
- label: lucide-react
- label: lucide-react-native
- label: lucide-solid
- label: lucide-static
- label: lucide-svelte (old version)
- label: '@lucide/svelte (new version)'
- label: lucide-vue
- label: lucide-vue-next
- label: lucide-astro
- 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
@@ -36,7 +38,7 @@ body:
attributes:
label: Version
description: What version of Lucide are you running?
placeholder: e.g. 1.1.0
placeholder: e.g. 0.289.1
validations:
required: true
- type: checkboxes
+10 -8
View File
@@ -13,19 +13,21 @@ body:
description: Which Lucide project do you wish this feature were added to? You may select more than one.
options:
- label: lucide
- label: lucide-react
- label: '@lucide/vue'
- label: '@lucide/svelte'
- label: '@lucide/angular'
- label: lucide-angular (old version)
- label: '@lucide/angular (new version)'
- label: '@lucide/astro'
- label: lucide-react-native
- label: lucide-flutter
- label: lucide-preact
- label: lucide-react
- label: lucide-react-native
- label: lucide-solid
- label: lucide-static
- label: lucide-svelte (old version)
- label: '@lucide/svelte (new version)'
- label: lucide-vue
- label: lucide-vue-next
- label: lucide-astro
- 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
-8
View File
@@ -1,8 +0,0 @@
## 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.
@@ -1,20 +0,0 @@
---
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 should not contain spaces. 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.
+10 -2
View File
@@ -52,13 +52,15 @@
💎 vue package:
- changed-files:
- any-glob-to-any-file:
- 'packages/vue/*'
- 'packages/lucide-vue/*'
- 'packages/lucide-vue-next/*'
# For changes in the Lucide angular package
🅰️ angular package:
- changed-files:
- any-glob-to-any-file:
- 'packages/angular/*'
- 'packages/lucide-angular/*'
# For changes in the Lucide preact package
⚛️ preact package:
@@ -70,7 +72,7 @@
🧣 svelte package:
- changed-files:
- any-glob-to-any-file:
- 'packages/svelte/*'
- 'packages/lucide-svelte/*'
# For changes in the Lucide solid package
🪝 solid package:
@@ -95,3 +97,9 @@
- 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/*'
+2 -2
View File
@@ -41,13 +41,13 @@ Common scopes: icons, docs, studio, site, dev
### Naming <!-- ONLY for new icons -->
<!-- All of these requirements must be fulfilled. -->
- [ ] I've read and followed the [naming conventions](https://lucide.dev/contribute/icon-design-guide#naming-conventions)
- [ ] I've read and followed the [naming conventions](https://lucide.dev/guide/design/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 <!-- ONLY for new icons -->
<!-- All of these requirements must be fulfilled. -->
- [ ] I've read and followed the [icon design guidelines](https://lucide.dev/contribute/icon-design-guide)
- [ ] I've read and followed the [icon design guidelines](https://lucide.dev/guide/design/icon-design-guide)
- [ ] 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.
+1 -30
View File
@@ -32,37 +32,8 @@ concurrency:
group: continuous-integration
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
outputs:
RELEASE_ENVIRONMENT: ${{ steps.environment.outputs.RELEASE_ENVIRONMENT }}
steps:
- uses: actions/checkout@v6
- 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 }}
if: github.repository == 'lucide-icons/lucide' && startsWith(github.event.head_commit.message, 'feat(icons)') && github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
@@ -33,7 +33,7 @@ jobs:
Lucide **does not accept** brand logos, and we do not plan to add them in the future. This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
[Click here to read our official statement about brand logos in Lucide.](https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md)
[Click here to read our official statement about brand logos in Lucide.](./BRAND_LOGOS_STATEMENT.md)
You can [search for similar issues.](https://github.com/lucide-icons/lucide/issues?q=is%3Aissue+${VALUE})
+44
View File
@@ -0,0 +1,44 @@
name: Lucide Angular checks
on:
push:
branches:
- next
pull_request:
paths:
- packages/lucide-angular/**
- tools/build-icons/**
- pnpm-lock.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
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@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-angular test
-3
View File
@@ -23,9 +23,6 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Execute unit tests for build-font
run: pnpm test:font
- name: Create font in ./lucide-font
run: pnpm build:font
+46
View File
@@ -0,0 +1,46 @@
name: Lucide Svelte checks
on:
push:
branches:
- next
pull_request:
paths:
- packages/lucide-svelte/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
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@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-svelte test
+46
View File
@@ -0,0 +1,46 @@
name: Lucide Vue Next checks
on:
push:
branches:
- next
pull_request:
paths:
- packages/lucide-vue-next/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- 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@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-vue-next test
@@ -1,4 +1,4 @@
name: Pull request metadata suggestions
name: Pull request tags suggestions
on:
pull_request_target:
@@ -6,8 +6,8 @@ on:
- 'icons/*.json'
jobs:
pull-request-metadata-suggestions:
name: Pull Request Metadata Suggestions
pull-request-tags-suggestions:
name: Pull Request Tags Suggestions
runs-on: ubuntu-latest
permissions:
contents: read
@@ -29,7 +29,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Generate comment markup
run: node ./scripts/suggestMetaData.mts
run: node ./scripts/suggestTags.mts
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PULL_REQUEST_NUMBER: ${{ github.event.number }}
+5 -5
View File
@@ -20,6 +20,9 @@ permissions:
id-token: write # Required for OIDC
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
prepare:
if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere", "jguddas"]', github.actor)
@@ -71,7 +74,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Set new version
run: pnpm version ${{ needs.prepare.outputs.VERSION }} --filter ${{ matrix.package }} --fail-if-no-match --no-git-tag-version
run: pnpm --fail-if-no-match --filter ${{ matrix.package }} version --new-version ${{ needs.prepare.outputs.VERSION }} --no-git-tag-version
- name: Build
run: pnpm --filter ${{ matrix.package }} build
@@ -105,7 +108,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Set new version
run: pnpm version ${{ needs.prepare.outputs.VERSION }} --filter lucide-static --no-git-tag-version
run: pnpm --filter lucide-static version --new-version ${{ needs.prepare.outputs.VERSION }} --no-git-tag-version
- name: Move Font
run: cp -r lucide-font packages/lucide-static/font
@@ -133,9 +136,6 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Execute unit tests for build-font
run: pnpm test:font
- name: Create font in ./lucide-font
run: pnpm build:font --saveCodePoints
env:
-42
View File
@@ -1,42 +0,0 @@
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:
runs-on: ubuntu-latest
steps:
- name: Set up Node.js environment
uses: actions/setup-node@v4
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"
-4
View File
@@ -26,11 +26,9 @@ packages/**/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
@@ -39,7 +37,6 @@ packages/**/LICENSE
categories.json
tags.json
.vercel
package-lock.json
# docs
docs/.vitepress/cache
@@ -53,7 +50,6 @@ docs/.vitepress/data/categoriesData.json
docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vitepress/data/brandStopwords.json
docs/.vitepress/data/iconPopularity
docs/.vercel
docs/.nitro
docs/public/og/**
+4 -5
View File
@@ -7,8 +7,8 @@ docs/.vitepress/cache
docs/.vitepress/data
docs/.nitro
# @lucide/angular
packages/angular/.angular
# lucide-angular
packages/lucide-angular/.angular/cache
# lucide-static
packages/lucide-static/icons
@@ -17,8 +17,7 @@ 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/svelte/src/icons/*.svelte
packages/svelte/.svelte-kit
packages/lucide-svelte/src/icons/*.svelte
packages/lucide-svelte/.svelte-kit
+1 -1
View File
@@ -39,7 +39,7 @@ This means adding them could:
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.
- Break [our own design rules](https://lucide.dev/guide/design/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".
+14 -19
View File
@@ -7,9 +7,9 @@
</a>
</p>
<p align="center">
<a href="https://github.com/lucide-icons/lucide/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-ISC-green" alt="license"></a>
<a href="https://github.com/lucide-icons/lucide/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/lucide" alt="license"></a>
<a href="https://www.figma.com/community/plugin/939567362549682242/Lucide-Icons"><img src="https://img.shields.io/badge/Figma-F24E1E?logo=figma&logoColor=white" alt="figma installs"></a>
<a href="https://github.com/lucide-icons/lucide/actions/workflows/ci.yml"><img src="https://github.com/lucide-icons/lucide/actions/workflows/ci.yml/badge.svg" alt="build status"></a>
<a href="https://github.com/lucide-icons/lucide/actions/workflows/release.yml"><img src="https://github.com/lucide-icons/lucide/actions/workflows/release.yml/badge.svg" alt="build status"></a>
<a href="https://discord.gg/EH6nSts"><img src="https://img.shields.io/discord/723074157486800936?label=chat&logo=discord&logoColor=%23ffffff&colorB=%237289DA" alt="discord chat"></a>
</p>
<p align="center">
@@ -26,22 +26,22 @@
# Lucide
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.
Lucide is an open-source icon library that provides 1000+ 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.
## Packages
| Logo | Package | Version | Downloads | Links |
| ---- | ------- | ------- | --------- | ----- |
| <img src="https://lucide.dev/framework-logos/js.svg" alt="JS logo" width="48"> | **`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) |
| <img src="https://lucide.dev/framework-logos/react.svg" alt="React logo" width="48"> | **`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) |
| <img src="https://lucide.dev/framework-logos/vue.svg" alt="Vue logo" width="48"> | **`@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) |
| <img src="https://lucide.dev/framework-logos/svelte.svg" alt="Svelte logo" width="48"> | **`@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) |
| <img src="https://lucide.dev/framework-logos/solid.svg" alt="Solid logo" width="48"> | **`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) |
| <img src="https://lucide.dev/framework-logos/preact.svg" alt="Preact logo" width="48"> | **`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) |
| <img src="https://lucide.dev/framework-logos/react-native.svg" alt="React Native logo" width="48"> | **`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) |
| <img src="https://lucide.dev/framework-logos/angular.svg" alt="Angular logo" width="48"> | **`@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) |
| <img src="https://lucide.dev/framework-logos/astro.svg" alt="Astro logo" width="48"> | **`@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) |
| <img src="https://lucide.dev/framework-logos/svg.svg" alt="SVG logo" width="48"> | **`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) |
| <img src="https://lucide.dev/framework-logos/js.svg" alt="JS logo" width="48"> | **`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/packages/lucide) · [Source](./packages/lucide) |
| <img src="https://lucide.dev/framework-logos/react.svg" alt="React logo" width="48"> | **`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/packages/lucide-react) · [Source](./packages/lucide-react) |
| <img src="https://lucide.dev/framework-logos/vue.svg" alt="Vue logo" width="48"> | **`lucide-vue-next`** | [![npm](https://img.shields.io/npm/v/lucide-vue-next)](https://www.npmjs.com/package/lucide-vue-next) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-vue-next) | [Docs](https://lucide.dev/guide/packages/lucide-vue-next) · [Source](./packages/lucide-vue-next) |
| <img src="https://lucide.dev/framework-logos/svelte.svg" alt="Svelte logo" width="48"> | **`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/packages/lucide-svelte) · [Source](./packages/lucide-svelte) |
| <img src="https://lucide.dev/framework-logos/solid.svg" alt="Solid logo" width="48"> | **`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/packages/lucide-solid) · [Source](./packages/lucide-solid) |
| <img src="https://lucide.dev/framework-logos/preact.svg" alt="Preact logo" width="48"> | **`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/packages/lucide-preact) · [Source](./packages/lucide-preact) |
| <img src="https://lucide.dev/framework-logos/react-native.svg" alt="React Native logo" width="48"> | **`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/packages/lucide-react-native) · [Source](./packages/lucide-react-native) |
| <img src="https://lucide.dev/framework-logos/angular.svg" alt="Angular logo" width="48"> | **`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/packages/lucide-angular) · [Source](./packages/lucide-angular) |
| <img src="https://lucide.dev/framework-logos/astro.svg" alt="Astro logo" width="48"> | **`@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/packages/lucide-astro) · [Source](./packages/astro) |
| <img src="https://lucide.dev/framework-logos/svg.svg" alt="SVG logo" width="48"> | **`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/packages/lucide-static) · [Source](./packages/lucide-static) |
### Figma
@@ -80,8 +80,6 @@ Thank you to all the people who contributed to Lucide!
<img src="https://opencollective.com/lucide-icons/contributors.svg?width=800" />
</a>
[//]: <> (Sponsors)
## Sponsors
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
@@ -90,10 +88,7 @@ Thank you to all the people who contributed to Lucide!
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="docs/public/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
### Hero backers 🦸
<a href="https://zephyr-cloud.io/"><img src="docs/public/sponsors/zephyr-cloud.svg" width="180" alt="Zephyr Cloud – From idea to prod: fast micro-frontend delivery!" /></a>
[//]: <> (Open Collective backers)
### Awesome backers 🍺
<a href="https://github.com/pdfme/pdfme"><img src="docs/public/sponsors/pdfme.svg" width="180" alt="pdfme – Open-source PDF generation library built with TypeScript and React." /></a>
+1
View File
@@ -54,6 +54,7 @@
"jira": "Jira",
"kotlin": "Kotlin",
"kubernetes": "Kubernetes",
"less": "Less",
"leetcode": "LeetCode",
"leet-code": "LeetCode",
"line": "LINE",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Navigation & Places",
"title": "Navigation, Maps, and POIs",
"icon": "compass"
}
@@ -11,7 +11,7 @@ export default eventHandler(async (event) => {
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 iconSize = parseInt(iconSizeString, 10);
const data = svgData.slice(0, -4);
const prevSvg = iconNodes[name]
+96 -14
View File
@@ -1,14 +1,15 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig, UserConfig } from 'vitepress';
import { defineConfig } 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 getStructuredData from './getStructuredData';
import { createGeneralOGImage, createIconOGImage } from './createOGImage';
import llmstxt from 'vitepress-plugin-llms';
import { transformPageData } from './transformPageData';
import getHeadConfig from './getHeadConfig';
import getHead from './getHead';
const defaultSandpackCSS = await readFile(
fileURLToPath(new URL('./theme/sandpack-default.css', import.meta.url)),
@@ -56,12 +57,6 @@ 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)),
@@ -79,11 +74,99 @@ export default defineConfig({
'brand-logo-statement.md',
'icons/**', // Not working, need investigation
],
}) as unknown as UserConfig['vite']['plugins'][0],
}),
],
},
head: getHeadConfig({ title, description, socialTitle }),
transformPageData,
head: getHead({ title, socialTitle, description }),
async transformPageData(pageData) {
pageData.frontmatter.head ??= [];
if (
pageData.relativePath.startsWith('icons/') &&
!pageData.relativePath.startsWith('icons/lab/') &&
pageData.params?.name
) {
const iconName = pageData.params.name;
pageData.title = `${iconName} icon details`;
const taggedAs = pageData.params?.tags?.length
? `Tagged as: ${pageData.params.tags.join(', ')}.`
: '';
const categorizedIn = pageData.params?.category?.length
? `Categorized in: ${pageData.params.category.join(', ')}.`
: '';
pageData.description =
`Details and related icons for ${iconName} icon. ${taggedAs} ${categorizedIn}`.trim();
const structuredData = await getStructuredData(iconName, pageData);
const ogPath = await createIconOGImage(iconName, pageData.params?.tags || []);
if (ogPath) {
const content = `https://lucide.dev${ogPath}`;
pageData.frontmatter.head.push(
[
'meta',
{
property: 'og:title',
content: pageData.title,
},
],
[
'meta',
{
property: 'og:description',
content: pageData.description,
},
],
[
'meta',
{
property: 'og:image',
content,
},
],
[
'meta',
{
property: 'twitter:image',
content,
},
],
);
}
pageData.frontmatter.head.push([
'script',
{ type: 'application/ld+json' },
JSON.stringify(structuredData),
]);
}
if (pageData.relativePath.startsWith('guide/')) {
const ogPath = await createGeneralOGImage(pageData);
if (ogPath) {
const content = `https://lucide.dev${ogPath}`;
pageData.frontmatter.head.push(
[
'meta',
{
property: 'og:image',
content,
},
],
[
'meta',
{
property: 'twitter:image',
content,
},
],
);
}
}
},
themeConfig: {
logo: {
light: '/logo.light.svg',
@@ -101,7 +184,6 @@ export default defineConfig({
},
{ text: 'Packages', link: '/packages' },
{ text: 'Showcase', link: '/showcase' },
{ text: 'Merch', link: 'https://merch.lucide.dev/' },
],
sidebar,
socialLinks: [
@@ -110,7 +192,7 @@ export default defineConfig({
],
footer: {
message: 'Released under the ISC License.',
copyright: `Copyright © ${new Date().getFullYear()} Lucide Icons`,
copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`,
},
editLink: {
pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path',
+1 -1
View File
@@ -97,7 +97,7 @@
},
{
"name": "navigation",
"title": "Navigation & Places"
"title": "Navigation, Maps, and POIs"
},
{
"name": "notifications",
+24 -7
View File
@@ -50,21 +50,20 @@
}
]
},
"@lucide/svelte": {
"lucide-svelte": {
"order": 3,
"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"
}
]
},
@@ -120,6 +119,24 @@
}
]
},
"lucide-angular": {
"order": 7,
"icon": "angular-old",
"description": "Our legacy Angular package with a module based API.",
"documentation": "https://v0.lucide.dev/guide/packages/lucide-angular",
"shields": [
{
"alt": "npm",
"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"
}
]
},
"lucide-preact": {
"order": 8,
"icon": "preact",
@@ -192,4 +209,4 @@
}
]
}
}
}
@@ -209,24 +209,5 @@
],
"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"
}
]
+208
View File
@@ -0,0 +1,208 @@
import { HeadConfig } from "vitepress"
interface GetHeadOptions {
title: string;
socialTitle: string;
description: string;
}
const brandColor = '#f56565'
const getHead = ({ title, socialTitle, description }: GetHeadOptions): HeadConfig[] => [
// Favicons and meta tags
[
'meta',
{
name: 'robots',
content: 'index,follow',
},
],
[
'meta',
{
name: 'theme-color',
content: brandColor,
},
],
[
'link',
{
rel: 'shortcut icon',
href: '/favicon.ico',
sizes: 'any',
},
],
[
'link',
{
rel: 'icon',
href: '/favicon.svg',
type: 'image/svg+xml',
},
],
[
'link',
{
rel: 'apple-touch-icon',
href: '/apple-touch-icon.png',
sizes: '180x180',
},
],
[
'link',
{
rel: 'icon',
type: 'image/png',
href: '/favicon-32x32.png',
sizes: '32x32',
},
],
[
'link',
{
rel: 'icon',
type: 'image/png',
href: '/favicon-16x16.png',
sizes: '16x16',
},
],
[
'link',
{
rel: 'mask-icon',
href: '/safari-pinned-tab.svg',
color: brandColor,
},
],
// Analytics
[
'link',
{
rel: 'preconnect',
href: 'https://analytics.lucide.dev',
},
],
[
'script',
{
src: 'https://analytics.lucide.dev/js/script.js',
'data-domain': 'lucide.dev',
defer: '',
},
],
// Open Graph and Twitter Card meta tags
[
'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: 'og:image:alt',
content: 'Lucide icon library preview image',
},
],
[
'meta',
{
property: 'twitter:card',
content: 'summary_large_image',
},
],
[
'meta',
{
property: 'twitter:site',
content: '@lucide_icons',
},
],
[
'meta',
{
property: 'twitter:title',
content: socialTitle,
},
],
[
'meta',
{
property: 'twitter:description',
content: description,
},
],
[
'meta',
{
property: 'twitter:image',
content: 'https://lucide.dev/og.png',
},
],
]
export default getHead
-127
View File
@@ -1,127 +0,0 @@
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;
+6 -2
View File
@@ -29,8 +29,12 @@ export default async function getStructuredData(iconName: string, pageData: Page
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`,
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`,
acquireLicensePage: 'https://lucide.dev/license',
license: 'https://lucide.dev/license',
copyrightNotice: 'Copyright (c) 2026 Lucide Icons and Contributors',
creditText: 'Lucide Icons',
encodingFormat: 'image/svg+xml',
keywords: pageData.params?.tags,
width: 24,
+1 -14
View File
@@ -459,20 +459,7 @@ const SvgPreview = (
/>
<ColoredPath
paths={paths}
colors={[
'#1982c4',
'#4267AC',
'#6a4c93',
'#B55379',
'#FF595E',
'#FF7655',
'#ff924c',
'#FFAE43',
'#ffca3a',
'#C5CA30',
'#8ac926',
'#52A675',
]}
colors={['#dfdfd6']}
/>
<Radii
paths={paths}
-5
View File
@@ -72,11 +72,6 @@ export const reactNativeSidebar = [
link: '/guide/react-native/advanced/combining-icons',
desc: 'Combine multiple icons into one',
},
{
text: 'Optimizations',
link: '/guide/react-native/advanced/optimizations',
desc: 'Optimizing your app for performance',
},
],
},
{
@@ -1,160 +0,0 @@
<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core';
import IconButton from './base/IconButton.vue';
import { x } from '../../data/iconNodes';
import Icon from '@lucide/vue/src/Icon';
import { computed, onMounted, ref } from 'vue';
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
const showAskSurvey = useLocalStorage('show-ask-overlay', true, {
initOnMounted: true,
});
const delayedShow = ref(false)
const showPopup = computed(() => showAskSurvey.value && delayedShow.value );
let isInitialized = false
function startTally() {
window.Tally.openPopup('EkvOpB', {
layout: 'modal',
width: 700,
autoClose: 5000,
onClose() {
showAskSurvey.value = false;
},
onSubmit() {
showAskSurvey.value = false;
},
});
}
onMounted(() => {
setTimeout(() => {
delayedShow.value = true
}, 10000)
})
function initTally() {
try {
if (!isInitialized) {
isInitialized = true
const s = document.createElement('script')
s.src = `//tally.so/widgets/embed.js`
s.async = true
s.onload = startTally
document.body.appendChild(s)
} else {
startTally();
}
} catch (error) {
window.open('https://tally.so/r/EkvOpB', '_blank');
}
}
function hidePopup() {
showAskSurvey.value = false;
}
</script>
<template>
<Teleport to="body">
<div
:class="{
'show-popup': showPopup,
}"
class="ask-overlay"
>
<IconButton
@click="hidePopup"
class="hide-button"
>
<Icon
:iconNode="x"
:size="20"
absoluteStrokeWidth
/>
</IconButton>
<h2 class="title">
We value your feedback!
</h2>
<p class="description">
Can you spare a moment to take our survey? <br>Your feedback helps us improve Lucide and make it better for everyone.
</p>
<div class="buttons">
<VPButton
text="Take the Survey"
theme="brand"
@click="initTally"
/>
<VPButton
text="Close"
theme="alt"
@click="hidePopup"
/>
</div>
</div>
</Teleport>
</template>
<style scoped>
.ask-overlay {
position: fixed;
z-index: 99;;
bottom: 24px;
width: calc(100% - 48px);
max-width: 420px;
left: 24px;
transition:
opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
padding: 20px;
border-radius: 12px;
box-shadow: var(--vp-shadow-4);
background-color: var(--vp-carbon-ads-bg-color);
opacity: 0;
transform: translateY(200px);
}
.ask-overlay .title {
line-height: 24px;
font-size: 16px;
font-weight: 600;
}
.ask-overlay .description {
padding-top: 8px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.ask-overlay.show-popup {
transform: translateY(0);
opacity: 1;
pointer-events: auto;
transition:
opacity 0.5s,
transform 0.25s ease;
}
.ask-overlay .buttons {
margin-top: 16px;
display: flex;
gap: 16px;
}
.hide-button {
padding: 4px;
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
background-color: transparent;
}
</style>
@@ -11,14 +11,6 @@ const { isDark } = useData();
const emit = defineEmits(['update:modelValue']);
function limitHexInput(val: string) {
if (val.startsWith('#')) {
return val.slice(0, 9);
}
return val.slice(0, 8);
}
const value = computed({
get: () => {
if (props.modelValue == null || props.modelValue === 'currentColor') {
@@ -27,7 +19,7 @@ const value = computed({
return props.modelValue;
},
set: (val) => emit('update:modelValue', limitHexInput(val)),
set: (val) => emit('update:modelValue', val),
});
</script>
@@ -49,7 +41,6 @@ const value = computed({
class="color-input-text"
aria-label="Color picker input"
v-model="value"
maxlength="9"
placeholder="[default]"
/>
</div>
@@ -100,7 +91,6 @@ const value = computed({
background: transparent;
color: var(--vp-c-text-1);
font-size: 13px;
font-family: var(--vp-font-family-mono);
text-align: left;
border-radius: 8px;
cursor: text;
@@ -1,22 +1,15 @@
<script setup lang="ts">
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';
import Icon from '@lucide/vue/src/Icon';
import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
</script>
<template>
<div class="top-notification">
<p>
<Icon
:iconNode="RocketDuotoneIcon"
size="18"
class="rocket-icon"
/>
<VPLink
class="notification-link"
href="/guide/version-1"
>Lucide v1 is out!</VPLink
>
>🚀<br />
You're looking at the site for v1, for v0 go to
<VPLink
class="notification-link"
@@ -28,7 +21,11 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
</template>
<style>
@media (min-width: 960px) {
:root {
--vp-layout-top-height: 64px;
}
@media (min-width: 640px) {
:root {
--vp-layout-top-height: 32px;
}
@@ -37,19 +34,19 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
<style scoped>
.top-notification {
position: sticky;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 9999;
display: flex;
height: var(--vp-layout-top-height);
justify-content: center;
align-items: center;
background: var(--vp-c-brand-dark);
color: var(--vp-c-bg-alt-down);
font-size: 14px;
padding: 4px 16px;
text-wrap: balance;
padding: 0 16px;
}
.notification-link {
@@ -61,9 +58,9 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
display: none !important;
}
.rocket-icon {
display: inline-block;
vertical-align: middle;
margin-right: 4px;
@media (min-width: 640px) {
.top-notification br {
display: none;
}
}
</style>
@@ -6,10 +6,10 @@ import SelectIcon from './SelectIcon.vue';
defineProps<{
id?: string;
items?: { name: string; value: string; icon?: string; iconDark?: string }[];
items?: { name: string; route: string; icon: string; iconDark?: string }[];
}>();
const selected = defineModel<{ name: string; value: string; icon?: string; iconDark?: string }>();
const selected = defineModel<{ name: string; icon: string; iconDark?: string }>();
</script>
<template>
@@ -19,18 +19,14 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
class="select-button"
:id="id"
>
<slot name="start-icon">
<SelectIcon v-if="selected.icon" :item="selected" />
</slot>
<SelectIcon :item="selected" />
<span class="select-text">{{ selected.name }}</span>
<span class="select-icon">
<slot name="end-icon">
<Icon
:iconNode="chevronsUpDown"
class="chevron-icon"
aria-hidden="true"
/>
</slot>
<Icon
:iconNode="chevronsUpDown"
class="chevron-icon"
aria-hidden="true"
/>
</span>
</ListboxButton>
@@ -47,9 +43,8 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
:value="item"
as="template"
>
<li :class="['select-option', { active, selected }]">
<SelectIcon v-if="item.icon" :item="item" />
<SelectIcon :item="item" />
<span :class="['option-text', { selected }]">{{ item.name }}</span>
<span
v-if="selected"
@@ -2,28 +2,25 @@
const { item } = defineProps<{
item: {
name: string;
icon?: string;
icon: string;
iconDark?: string;
};
}>();
</script>
<template>
<div v-if="item.icon || item.iconDark">
<img
v-if="item.icon"
:src="item.icon"
:class="{ 'select-item-icon': true, light: item.iconDark }"
:alt="`${item.name} logo`"
loading="lazy"
/>
<img
v-if="item.iconDark"
:src="item.iconDark"
:alt="`${item.name} logo`"
class="select-item-icon dark"
/>
</div>
<img
:src="item.icon"
:class="{ 'select-item-icon': true, light: item.iconDark }"
:alt="`${item.name} logo`"
loading="lazy"
/>
<img
v-if="item.iconDark"
:src="item.iconDark"
:alt="`${item.name} logo`"
class="select-item-icon dark"
/>
</template>
<style scoped>
@@ -11,49 +11,49 @@ const { page } = useData();
const router = useRouter();
const frameworks = [
{ name: 'Vanilla', icon: '/framework-logos/js.svg', value: '/guide/lucide/' },
{ name: 'React', icon: '/framework-logos/react.svg', value: '/guide/react/' },
{ name: 'Vue', icon: '/framework-logos/vue.svg', value: '/guide/vue/' },
{ name: 'Svelte', icon: '/framework-logos/svelte.svg', value: '/guide/svelte/' },
{ name: 'Solid', icon: '/framework-logos/solid.svg', value: '/guide/solid/' },
{ name: 'Angular', icon: '/framework-logos/angular.svg', value: '/guide/angular/' },
{ name: 'Preact', icon: '/framework-logos/preact.svg', value: '/guide/preact/' },
{ name: 'Vanilla', icon: '/framework-logos/js.svg', route: '/guide/lucide/' },
{ name: 'React', icon: '/framework-logos/react.svg', route: '/guide/react/' },
{ name: 'Vue', icon: '/framework-logos/vue.svg', route: '/guide/vue/' },
{ name: 'Svelte', icon: '/framework-logos/svelte.svg', route: '/guide/svelte/' },
{ name: 'Solid', icon: '/framework-logos/solid.svg', route: '/guide/solid/' },
{ name: 'Angular', icon: '/framework-logos/angular.svg', route: '/guide/angular/' },
{ name: 'Preact', icon: '/framework-logos/preact.svg', route: '/guide/preact/' },
{
name: 'React Native',
icon: '/framework-logos/react-native.svg',
value: '/guide/react-native/',
route: '/guide/react-native/',
},
{
name: 'Astro',
icon: '/framework-logos/astro.svg',
iconDark: '/framework-logos/astro-dark.svg',
value: '/guide/astro/',
route: '/guide/astro/',
},
{ name: 'Static', icon: '/framework-logos/svg.svg', value: '/guide/static/' },
{ name: 'Static', icon: '/framework-logos/svg.svg', route: '/guide/static/' },
];
const fallbackFramework = useLocalStorage('lucide-docs-fallback-framework', frameworks[1]);
const selected = computed(() => {
const current = frameworks.find(({ value }) => {
const current = frameworks.find(({ route }) => {
if (router.route.path?.startsWith?.('/guide')) {
const [, , framework] = router.route.path.split('/');
const [, frameWorkRoute] = value.split('/').filter(Boolean);
const [, frameWorkRoute] = route.split('/').filter(Boolean);
return framework === frameWorkRoute;
}
return router.route.path.split('/').slice(0, 3).join('/') === value;
return router.route.path.split('/').slice(0, 3).join('/') === route;
});
return current || (fallbackFramework.value?.value ? fallbackFramework.value : frameworks[0]);
return current || fallbackFramework.value;
});
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; value: string }) {
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; route: string }) {
fallbackFramework.value = item;
if (item.value !== router.route.path) {
const likeRoute = router.route.path.replace(selected.value.value, item.value);
if (item.route !== router.route.path) {
const likeRoute = router.route.path.replace(selected.value.route, item.route);
const hasRoute = sidebar[item.value]?.some((section) =>
const hasRoute = sidebar[item.route]?.some((section) =>
section?.items?.some(({ link }) => link === likeRoute),
);
@@ -62,7 +62,7 @@ function onSelectFramework(item: { name: string; icon: string; iconDark?: string
return;
}
router.go(item.value);
router.go(item.route);
}
}
</script>
@@ -85,12 +85,12 @@ function onSelectFramework(item: { name: string; icon: string; iconDark?: string
/>
</div>
<VPSidebarGroup
:key="selected.value"
:key="selected.route"
v-if="
page?.relativePath?.startsWith?.('guide') &&
!page?.relativePath?.startsWith?.(selected.value.substring(1))
!page?.relativePath?.startsWith?.(selected.route.substring(1))
"
:items="sidebar[selected.value]"
:items="sidebar[selected.route]"
/>
</template>
@@ -118,15 +118,5 @@ label {
font-weight: bold;
margin-bottom: 4px;
display: block;
position: relative;
}
.framework-select:before {
content: ' ';
position: absolute;
inset: 0 -16px -24px;
background: color-mix(in srgb, var(--vp-c-bg-alt), transparent 50%);
backdrop-filter: blur(4px);
mask-image: linear-gradient(to bottom, black calc(100% - 16px), transparent 100%);
}
</style>
@@ -3,7 +3,25 @@ import Badge from '../base/Badge.vue';
import { moveRight } from '../../../data/iconNodes';
import Icon from '@lucide/vue/src/Icon';
import { data } from './HomeHeroInfoBefore.data';
import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
const rocketDuoTone = [
[
'path',
{
d: 'M8 18a2 2 0 0 0-2-2c-2.53-.078-3.247 3.605-3.5 5.5 1.895-.253 5.578-.97 5.5-3.5',
fill: 'currentColor',
opacity: 0.5,
},
],
[
'path',
{
d: 'M22 2c0 2.72-.78 7.5-6 11 0 0 1.08 3.38 0 5-.969 1.448-4 2-4 2v-5l-3-3H4s.553-3.033 2-4c1.616-1.077 4.982.043 5 .05A12.88 12.88 0 0 1 22 2m-6 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6',
fill: 'currentColor',
'fill-rule': 'evenodd',
'clip-rule': 'evenodd',
},
],
] as const;
</script>
<template>
@@ -15,7 +33,7 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
class="badge-special confetti-button animate"
href="/guide/version-1"
>
<Icon :iconNode="RocketDuotoneIcon" />
<Icon :iconNode="rocketDuoTone" />
Version 1
<Icon
:iconNode="moveRight"
@@ -13,21 +13,23 @@ const { go } = useRouter()
<HomeSectionTitle>Available For:</HomeSectionTitle>
<div class="packages-list">
<a
v-for="{ name, label, path, logo, logoDark } in data.packages"
:href="path"
v-for="{ name, logo, logoDark } in data.packages"
:href="`/guide/packages/${name}`"
class="package-logo"
@click.prevent="go(path)"
:aria-label="`Read more about: ${name} package`"
@click.prevent="go(`/guide/packages/${name}`)"
>
<img
:src="logo"
:class="{ light: logoDark, 'image-logo': true }"
:alt="label"
:alt="`${name} logo`"
loading="lazy"
/>
<img
v-if="logoDark"
:src="logoDark"
:alt="label"
:alt="`${name} logo`"
class="image-logo dark"
loading="lazy"
/>
@@ -1,21 +0,0 @@
const RocketDuotoneIcon = [
[
'path',
{
d: 'M8 18a2 2 0 0 0-2-2c-2.53-.078-3.247 3.605-3.5 5.5 1.895-.253 5.578-.97 5.5-3.5',
fill: 'currentColor',
opacity: 0.5,
},
],
[
'path',
{
d: 'M22 2c0 2.72-.78 7.5-6 11 0 0 1.08 3.38 0 5-.969 1.448-4 2-4 2v-5l-3-3H4s.553-3.033 2-4c1.616-1.077 4.982.043 5 .05A12.88 12.88 0 0 1 22 2m-6 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6',
fill: 'currentColor',
'fill-rule': 'evenodd',
'clip-rule': 'evenodd',
},
],
] as const;
export default RocketDuotoneIcon;
@@ -5,18 +5,11 @@ import IconButton from '../base/IconButton.vue';
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue';
import { x } from '../../../data/iconNodes';
import Icon from '@lucide/vue/src/Icon';
import { computed, onMounted, ref } from 'vue';
import { onMounted, ref } from 'vue';
const { theme } = useData();
const showAd = useSessionStorage('show-carbon-ads', true, {
initOnMounted: true,
});
const showAd = useSessionStorage('show-carbon-ads', true);
const carbonLoaded = ref(true);
const shouldHideAd = computed(() => !showAd.value || !carbonLoaded.value);
function hideAd() {
showAd.value = false;
}
defineProps<{
drawerOpen: boolean;
@@ -35,13 +28,13 @@ onMounted(() => {
<div
:class="{
'drawer-open': drawerOpen,
'hide-ad': shouldHideAd,
'hide-ad': !(showAd && carbonLoaded),
}"
class="floating-ad"
v-if="theme.carbonAds"
>
<IconButton
@click="hideAd"
@click="showAd = false"
class="hide-button"
>
<Icon
@@ -50,7 +43,7 @@ onMounted(() => {
absoluteStrokeWidth
/>
</IconButton>
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" />
</div>
</template>
@@ -73,7 +66,6 @@ onMounted(() => {
.floating-ad.hide-ad {
transform: translateX(224px);
opacity: 0;
pointer-events: none;
}
.floating-ad.drawer-open.hide-ad {
@@ -104,7 +96,6 @@ onMounted(() => {
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
background-color: transparent;
}
</style>
@@ -7,14 +7,10 @@ import { useActiveAnchor } from '../../composables/useActiveAnchor'
import { data } from './CategoryList.data'
import CategoryListItem from './CategoryListItem.vue'
import SidebarTitle from './SidebarTitle.vue'
import { useCategoryViewSync } from '../../composables/useCategoryView'
import { useCategoryView } from '../../composables/useCategoryView'
const { page } = useData()
const { categoryCounts, selectedCategory } = useCategoryViewSync();
function onCategoriesClick() {
selectedCategory.value = '';
}
const { categoryCounts } = useCategoryView();
const categoriesIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/categories');
@@ -57,7 +53,7 @@ watch(headers, () => {
<VPLink class="sidebar-link sidebar-text" href="/icons/" :class="{ 'active': overviewIsActive } ">
All
</VPLink>
<VPLink class="sidebar-link sidebar-text" href="/icons/categories" :class="{ 'active': categoriesIsActive }" @click="onCategoriesClick">
<VPLink class="sidebar-link sidebar-text" href="/icons/categories" :class="{ 'active': categoriesIsActive } ">
Categories
</VPLink>
<div class="content">
@@ -28,10 +28,7 @@ function onClick(categoryName: string) {
const heading = document.querySelector<HTMLAnchorElement>(categoryName);
heading?.focus();
const url = new URL(window.location.href);
url.pathname = '/icons/categories';
url.hash = categoryName;
window.history.pushState({}, '', url);
window.history.pushState({}, '', `/icons/categories#${categoryName}`)
}
</script>
@@ -88,7 +85,6 @@ function onClick(categoryName: string) {
.outline-link.inactive {
color: var(--vp-c-text-4);
pointer-events: none;
}
.outline-link.nested {
@@ -188,7 +188,7 @@ const deprecatedTitle = computed(() => {
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
scrollbar-color: currentColor transparent; /* foreground background */
}
.horizontal-scroller::-webkit-scrollbar {
width: 0;
@@ -3,7 +3,6 @@ import type { IconEntity } from '../../types';
import { computed, ref } from 'vue';
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
import { useIconStyleContext } from '../../composables/useIconStyle';
import { squarePen } from '../../../data/iconNodes';
const props = defineProps<{
name: IconEntity['name'];
@@ -20,31 +19,6 @@ const iconComponent = computed(() => {
if (!props.name || !props.iconNode) return null;
return createLucideIcon(props.name, props.iconNode);
});
const SquarePenIcon = createLucideIcon('SquarePen', squarePen);
const innerSvg = computed(() => {
if (!props.iconNode) return '';
const string = props.iconNode
.map(([tag, attrs]) => {
const attributes = Object.entries(attrs)
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
return `<${tag} ${attributes}/>`;
})
.join('');
return string
.replace(/>[\r\n ]+</g, '><')
.replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
.trim();
});
const base64InnerSvg = computed(() => {
if (typeof window === 'undefined') return '';
return btoa(innerSvg.value);
});
</script>
<template>
@@ -83,16 +57,6 @@ const base64InnerSvg = computed(() => {
/>
</g>
</svg>
<a
:href="`https://studio.lucide.dev/edit?value=${encodeURIComponent(base64InnerSvg)}&name=${name}`"
target="_blank"
rel="noopener noreferrer"
class="studio-button"
title="Open in Lucide Studio"
aria-label="Open in Lucide Studio"
>
<SquarePenIcon :size="14" />
</a>
</div>
</template>
@@ -111,8 +75,6 @@ const base64InnerSvg = computed(() => {
position: relative;
background: var(--vp-c-bg-alt);
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.icon-container > :deep(svg:not(.icon-grid)) {
width: 100%;
@@ -123,34 +85,6 @@ const base64InnerSvg = computed(() => {
opacity: 0.8;
}
.studio-button {
position: absolute;
bottom: 8px;
right: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
color: var(--vp-c-text-2);
opacity: 0;
transition: opacity 0.2s, color 0.2s, background-color 0.2s;
z-index: 2;
}
.icon-container:hover .studio-button,
.studio-button:focus-visible {
opacity: 1;
}
.studio-button:hover,
.studio-button:focus-visible {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.icon-component.customizable {
will-change: width, height, stroke-width, stroke;
/* color: var(--customize-color, currentColor);
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, watch, defineAsyncComponent, onMounted } from 'vue';
import { ref, computed, defineAsyncComponent, onMounted } from 'vue';
import type { IconEntity, Category } from '../../types';
import useSearch from '../../composables/useSearch';
import InputSearch from '../base/InputSearch.vue';
@@ -12,7 +12,6 @@ import useFetchCategories from '../../composables/useFetchCategories';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import chunkArray from '../../utils/chunkArray';
import useScrollToCategory from '../../composables/useScrollToCategory';
import { useCategoryView } from '../../composables/useCategoryView';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
@@ -27,15 +26,8 @@ const props = defineProps<{
const activeIconName = ref(null);
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const { selectedCategory } = useCategoryView();
const isSearching = computed(() => !!searchQuery.value);
watch(searchQueryDebounced, (searchString) => {
if (searchString !== '') {
selectedCategory.value = '';
}
});
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
@@ -44,12 +36,8 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categoriesMap,
isFetching: isFetchingCategories,
} = useFetchCategories();
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -121,9 +109,6 @@ const categoriesList = computed(() => {
}, []);
});
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && (tags.value == null || categoriesMap.value == null),
);
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
itemHeight: ICON_SIZE + ICON_GRID_GAP,
@@ -142,39 +127,22 @@ onMounted(() => {
useEventListener(window, 'scroll', containerProps.onScroll);
});
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
}
if (categoriesMap.value == null && !isFetchingCategories.value) {
void fetchCategories();
if (categoriesMap.value == null) {
fetchCategories();
}
}
watch(searchQuery, (searchString) => {
if (searchString !== '') {
loadSearchMetadata();
}
});
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
function handleCloseDrawer() {
setActiveIconName('');
const url = new URL(window.location);
url.pathname = '/icons/categories';
if (searchQueryDebounced.value) {
url.searchParams.set('search', searchQueryDebounced.value);
}
if (selectedCategory.value) {
url.hash = selectedCategory.value;
}
window.history.pushState({}, '', url);
window.history.pushState({}, '', '/icons/categories');
}
</script>
@@ -182,7 +150,6 @@ function handleCloseDrawer() {
<div
ref="overviewEl"
class="overview-container"
:class="{ 'icon-drawer-open': activeIconName }"
>
<StickyBar class="category-search">
<InputSearch
@@ -191,11 +158,11 @@ function handleCloseDrawer() {
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
@focus="loadSearchMetadata"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
v-if="searchPlaceholder.isNoResults"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"
@@ -4,7 +4,6 @@ import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import { useRoute } from 'vitepress';
import IconGrid from './IconGrid.vue';
import Select from '../base/Select.vue';
import InputSearch from '../base/InputSearch.vue';
import useSearch from '../../composables/useSearch';
import useSearchInput from '../../composables/useSearchInput';
@@ -15,26 +14,9 @@ import useFetchCategories from '../../composables/useFetchCategories';
import chunkArray from '../../utils/chunkArray';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
import Icon from '@lucide/vue/src/Icon';
import { listSortDescending } from '~/.vitepress/data/iconNodes';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const SORTING = [
{
name: 'Popularity',
value: 'popularity',
},
{
name: 'Release date',
value: 'release-date',
},
{
name: 'Name',
value: 'name',
},
]
const initialGridItems = computed(() => {
if (containerWidth.value === 0) return 120;
@@ -50,14 +32,9 @@ const props = defineProps<{
}>();
const activeIconName = ref(null);
const selectedSort = ref(SORTING[0])
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categories,
isFetching: isFetchingCategories,
} = useFetchCategories();
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -66,29 +43,12 @@ const columnSize = computed(() => {
return Math.floor(containerWidth.value / (ICON_SIZE + ICON_GRID_GAP));
});
const sortedIcons = computed(() => {
switch (selectedSort.value.value) {
case 'popularity':
return [...props.icons].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
case 'release-date':
return [...props.icons].sort((a, b) => {
const aDate = a.createdRelease?.date ? new Date(a.createdRelease.date).getTime() : 0;
const bDate = b.createdRelease?.date ? new Date(b.createdRelease.date).getTime() : 0;
return bDate - aDate;
});
case 'name':
return [...props.icons].sort((a, b) => a.name.localeCompare(b.name));
default:
return props.icons;
}
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return sortedIcons.value;
return props.icons;
}
return sortedIcons.value.map((icon) => {
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name];
const iconCategories = categories.value?.[icon.name] ?? [];
@@ -108,15 +68,11 @@ const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 8 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
]);
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && (tags.value == null || categories.value == null),
);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
@@ -142,21 +98,15 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
}
if (categories.value == null && !isFetchingCategories.value) {
void fetchCategories();
if (categories.value == null) {
fetchCategories();
}
}
watch(searchQuery, (searchString) => {
if (searchString !== '') {
loadSearchMetadata();
}
});
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
@@ -168,14 +118,7 @@ watch(searchQueryDebounced, () => {
function handleCloseDrawer() {
setActiveIconName('');
const url = new URL(window.location);
url.pathname = '/icons/';
if (searchQueryDebounced.value) {
url.searchParams.set('search', searchQueryDebounced.value);
}
window.history.pushState({}, '', url);
window.history.pushState({}, '', '/icons/');
}
</script>
@@ -183,7 +126,6 @@ function handleCloseDrawer() {
<div
ref="overviewEl"
class="overview-container"
:class="{ 'icon-drawer-open': activeIconName }"
>
<StickyBar>
<InputSearch
@@ -192,25 +134,11 @@ function handleCloseDrawer() {
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@focus="loadSearchMetadata"
@focus="onFocusSearchInput"
/>
<Select
id="sort-select"
:items="SORTING"
v-model="selectedSort"
>
<template #start-icon>
<Icon
:iconNode="listSortDescending"
class="chevron-icon"
aria-hidden="true"
/>
</template>
</Select>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
v-if="searchPlaceholder.isNoResults"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"
@@ -73,11 +73,13 @@ const customizingActive = computed(() => {
id="icon-color"
label="Color"
>
<ColorPicker
v-model="color"
id="icon-color"
class="color-picker"
/>
<template #display>
<ColorPicker
v-model="color"
id="icon-color"
class="color-picker"
/>
</template>
</InputField>
<InputField
@@ -159,6 +161,6 @@ const customizingActive = computed(() => {
}
.color-picker {
width: 100%;
margin-left: auto;
}
</style>
@@ -9,63 +9,20 @@
margin-bottom: 16px;
position: sticky;
z-index: 10;
top: var(--vp-layout-top-height);
top: 48px;
padding-top: 24px;
margin-top: -32px;
width: 100%;
display: flex;
margin-bottom: 32px;
background: var(--vp-c-bg);
box-shadow: 0 16px 24px var(--vp-c-bg);
gap: 16px;
}
.search-bar :deep(.select-button) {
padding: 11px 14px;
background-color: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
flex-shrink: 0;
flex: 1;
font-weight: 500;
cursor: pointer;
}
.search-bar :deep(.select-button):hover,
.search-bar :deep(.select-button):focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.search-bar :deep(.select-options) {
min-width: 160px;
}
.search-bar :deep(.select-option) {
padding-right: 36px;
padding-left: 8px;
}
.search-bar :deep(.select-icon) {
display: none;
}
.search-bar :deep(.select-button) .select-text {
display: none;
}
@media (min-width: 640px) {
.search-bar :deep(.select-button) {
padding: 11px 16px 11px 16px;
}
.search-bar :deep(.select-button) .select-text {
display: block;
}
}
@media (min-width: 960px) {
.search-bar {
padding-top: 32px;
margin-top: calc((var(--vp-layout-top-height, 0px) + 32px) * -1);
top: calc(var(--vp-layout-top-height, 0px) + 64px);
top: 64px;
}
}
</style>
@@ -1,133 +0,0 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import { ref, watch, onMounted } from 'vue'
import { useAside } from 'vitepress/dist/client/theme-default/composables/aside.js'
import { useData } from 'vitepress/dist/client/theme-default/composables/data.js'
const { page } = useData()
const props = defineProps<{
carbonAds: DefaultTheme.CarbonAdsOptions
overlay?: boolean
}>()
const carbonOptions = props.carbonAds
const { isAsideEnabled } = useAside()
const container = ref()
let isInitialized = false
function init() {
if (!isInitialized) {
isInitialized = true
const params = new URLSearchParams({
serve: carbonOptions.code,
placement: carbonOptions.placement,
format: 'responsive',
})
const s = document.createElement('script')
s.id = '_carbonads_js'
s.src = `//cdn.carbonads.com/carbon.js?${params.toString()}`
s.async = true
container.value.appendChild(s)
}
}
watch(() => page.value.relativePath, () => {
if (isInitialized && isAsideEnabled.value) {
;(window as any)._carbonads?.refresh()
}
})
// no need to account for option changes during dev, we can just
// refresh the page
if (carbonOptions) {
onMounted(() => {
// if the page is loaded when aside is active, load carbon directly.
// otherwise, only load it if the page resizes to wide enough. this avoids
// loading carbon at all on mobile where it's never shown
if (isAsideEnabled.value || props.overlay) {
init()
} else {
watch(isAsideEnabled, (wide) => wide && init())
}
})
}
</script>
<template>
<div class="VPCarbonAds" ref="container" />
</template>
<style scoped>
.VPCarbonAds {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
border-radius: 12px;
min-height: 256px;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 500;
background-color: var(--vp-carbon-ads-bg-color);
}
.VPCarbonAds :deep(img) {
margin: 0 auto;
border-radius: 6px;
}
.VPCarbonAds :deep(.carbon-text) {
display: block;
margin: 0 auto;
padding-top: 12px;
color: var(--vp-carbon-ads-text-color);
transition: color 0.25s;
}
.VPCarbonAds :deep(.carbon-text:hover) {
color: var(--vp-carbon-ads-hover-text-color);
}
.VPCarbonAds :deep(#carbon-responsive .carbon-poweredby),
.VPCarbonAds :deep(.carbon-poweredby) {
display: block;
padding-bottom: 2px;
font-size: 11px;
font-weight: 500;
color: var(--vp-carbon-ads-poweredby-color);
text-transform: uppercase;
transition: color 0.25s;
}
.VPCarbonAds :deep(#carbon-responsive) {
font-size: 12px;
line-height: 18px;
font-weight: 500;
}
.VPCarbonAds :deep(#carbon-responsive .carbon-poweredby) {
text-align: left;
}
.VPCarbonAds :deep(#carbon-responsive .carbon-poweredby:hover),
.VPCarbonAds :deep(.carbon-poweredby:hover) {
color: var(--vp-carbon-ads-hover-poweredby-color);
}
.VPCarbonAds :deep(> div) {
display: none;
}
.VPCarbonAds :deep(> div:first-of-type) {
display: block;
}
.VPCarbonAds :deep(#carbon-responsive) {
flex-direction: column-reverse;
}
.VPCarbonAds :deep(#carbon-responsive .carbon-responsive-wrap) {
border-radius: 10px;
}
</style>
@@ -3,7 +3,6 @@ import { useData } from 'vitepress';
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar';
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';
import { computed } from 'vue';
import SurveyAskOverlay from '../SurveyAskOverlay.vue';
const { theme } = useData();
const { hasSidebar } = useSidebar();
@@ -1,5 +1,5 @@
import { useRoute } from 'vitepress';
import { ref, inject, Ref, onMounted, onUnmounted, watch } from 'vue';
import { ref, inject, Ref, onMounted, watch } from 'vue';
export const CATEGORY_VIEW_CONTEXT = Symbol('categoryView');
@@ -13,53 +13,25 @@ export const categoryViewContext = {
categoryCounts: ref({}),
};
const CATEGORIES_PATH = '/icons/categories';
export function useCategoryView(): CategoryViewContext {
const context = inject<CategoryViewContext>(CATEGORY_VIEW_CONTEXT);
const route = useRoute();
if (!context) {
throw new Error('useCategoryView must be used with categoryView context');
}
return context;
}
export function useCategoryViewSync(): CategoryViewContext {
const context = useCategoryView();
const route = useRoute();
function syncCategoryFromLocation() {
if (!window.location.pathname.startsWith(CATEGORIES_PATH)) {
context.selectedCategory.value = '';
context.categoryCounts.value = {};
return;
}
context.selectedCategory.value = window.location.hash
? decodeURIComponent(window.location.hash.slice(1))
: '';
}
onMounted(() => {
syncCategoryFromLocation();
window.addEventListener('hashchange', syncCategoryFromLocation);
window.addEventListener('popstate', syncCategoryFromLocation);
});
onUnmounted(() => {
window.removeEventListener('hashchange', syncCategoryFromLocation);
window.removeEventListener('popstate', syncCategoryFromLocation);
if (window.location.hash) {
context.selectedCategory.value = decodeURIComponent(window.location.hash.slice(1));
}
});
watch(route, (currentRoute) => {
if (currentRoute.path !== CATEGORIES_PATH) {
if (currentRoute.path !== '/icons/categories') {
context.selectedCategory.value = '';
context.categoryCounts.value = {};
return;
}
syncCategoryFromLocation();
});
return context;
@@ -34,13 +34,7 @@ export default function useScrollToCategory({
}
}
watch(selectedCategory, (value) => {
if (value) {
scrollToSelectedCategory(value);
} else {
scrollTo(0);
}
});
watch(selectedCategory, scrollToSelectedCategory);
onMounted(() => {
setTimeout(() => {
@@ -49,11 +43,7 @@ export default function useScrollToCategory({
});
watch(searchQueryDebounced, () => {
if (selectedCategory.value) {
scrollToSelectedCategory(selectedCategory.value);
} else {
scrollTo(0);
}
scrollTo(0);
});
watch(categories, (items) => {
@@ -20,6 +20,16 @@ const useSearch = <T>(
return index.value.search(query.value).map((result) => result.item);
}
if (keys.length !== 0) {
const mainKey = keys[0].name;
return collection.value.sort((a, b) => {
const aString = a[mainKey as keyof T] as string;
const bString = b[mainKey as keyof T] as string;
return aString.localeCompare(bString);
});
}
return collection.value;
});
@@ -1,5 +1,5 @@
import { useDebounce } from '@vueuse/core';
import { onMounted, ref, watch } from 'vue';
import { nextTick, onMounted, ref, watch } from 'vue';
const useSearchInput = () => {
const searchInput = ref();
@@ -13,10 +13,11 @@ const useSearchInput = () => {
newUrl.searchParams.delete('search');
} else {
newUrl.searchParams.set('search', searchString);
newUrl.hash = '';
}
window.history.replaceState({}, '', newUrl);
nextTick(() => {
window.history.replaceState({}, '', newUrl);
});
});
onMounted(() => {
+2 -2
View File
@@ -7,6 +7,7 @@ import IconsSidebarNavAfter from './layouts/IconsSidebarNavAfter.vue';
import HomeHeroIconsCard from './components/home/HomeHeroIconsCard.vue';
import HomeHeroAfter from './components/home/HomeHeroAfter.vue';
import HomeHeroInfoBefore from './components/home/HomeHeroInfoBefore.vue';
import LayoutTop from './components/base/LayoutTop.vue';
import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle';
import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView';
import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs';
@@ -22,8 +23,7 @@ const theme: Partial<Theme> = {
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
'home-hero-image': () => h(HomeHeroIconsCard),
'home-hero-actions-after': () => h(HomeHeroAfter),
// Keep this here for now, we might want to add more things to the top bar in the future
// 'layout-top': () => h(LayoutTop),
'layout-top': () => h(LayoutTop),
});
},
enhanceApp({ app }) {
-29
View File
@@ -67,10 +67,6 @@
animation: .3s transition 0s ease;
} */
html {
scrollbar-gutter: stable;
}
.VPHome {
overflow-x: hidden;
}
@@ -121,10 +117,6 @@ html {
padding-left: 0;
}
.overview-container.icon-drawer-open {
padding-bottom: 288px;
}
.VPHomeHero .container {
flex-direction: column-reverse;
}
@@ -162,24 +154,3 @@ html {
margin: 0 auto 48px;
max-width: 480px;
}
.VPSidebar {
/* Hide Scrollbar */
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
}
.VPSidebar::-webkit-scrollbar {
width: 0;
display: none;
}
.VPSidebar::-webkit-scrollbar-track {
background: transparent;
}
.VPSidebar::-webkit-scrollbar-thumb {
background: transparent;
border: none;
}
-1
View File
@@ -18,7 +18,6 @@ export interface IconEntity extends IconMetaData {
externalLibrary?: ExternalLibs;
createdRelease?: Release;
changedRelease?: Release;
popularity?: number;
}
export interface Category {
-79
View File
@@ -1,79 +0,0 @@
import { type PageData } from 'vitepress';
import getStructuredData from './getStructuredData';
import { createGeneralOGImage, createIconOGImage } from './createOGImage';
export async function transformPageData(pageData: PageData) {
pageData.frontmatter.head ??= [];
if (
pageData.relativePath.startsWith('icons/') &&
!pageData.relativePath.startsWith('icons/lab/') &&
pageData.params?.name
) {
const iconName = pageData.params.name;
pageData.title = `${iconName} icon details`;
const taggedAs = pageData.params?.tags?.length
? `Tagged as: ${pageData.params.tags.join(', ')}.`
: '';
const categorizedIn = pageData.params?.category?.length
? `Categorized in: ${pageData.params.category.join(', ')}.`
: '';
pageData.description =
`Details and related icons for ${iconName} icon. ${taggedAs} ${categorizedIn}`.trim();
const structuredData = await getStructuredData(iconName, pageData);
const ogPath = await createIconOGImage(iconName, pageData.params?.tags || []);
if (ogPath) {
const content = `https://lucide.dev${ogPath}`;
pageData.frontmatter.head.push(
[
'meta',
{
property: 'og:image',
content,
},
],
[
'meta',
{
property: 'twitter:image',
content,
},
],
);
}
pageData.frontmatter.head.push([
'script',
{ type: 'application/ld+json' },
JSON.stringify(structuredData),
]);
}
if (pageData.relativePath.startsWith('guide/')) {
const ogPath = await createGeneralOGImage(pageData);
if (ogPath) {
const content = `https://lucide.dev${ogPath}`;
pageData.frontmatter.head.push(
[
'meta',
{
property: 'og:image',
content,
},
],
[
'meta',
{
property: 'twitter:image',
content,
},
],
);
}
}
}
-38
View File
@@ -46,43 +46,5 @@ declare global {
*/
append(container: Element): void;
};
Tally: {
// From https://tally.so/help/developer-resources#2f676e40530a460ea6a634b8441f6001
openPopup: (
formId: string,
options?: {
key?: string; // This is used as a unique identifier used for the "Show only once" and "Don't show after submit" functionality
layout?: 'default' | 'modal';
width?: number;
alignLeft?: boolean;
hideTitle?: boolean;
overlay?: boolean;
emoji?: {
text: string;
animation:
| 'none'
| 'wave'
| 'tada'
| 'heart-beat'
| 'spin'
| 'flash'
| 'bounce'
| 'rubber-band'
| 'head-shake';
};
autoClose?: number; // in milliseconds
showOnce?: boolean;
doNotShowAfterSubmit?: boolean;
customFormUrl?: string; // when you want to load the form via it's custom domain URL
hiddenFields?: {
[key: string]: any;
};
onOpen?: () => void;
onClose?: () => void;
onPageView?: (page: number) => void;
onSubmit?: (payload: SubmissionPayload) => void;
},
) => void;
};
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ However, not everyone can understand them easily. Read more about [how to use Lu
## Official Packages
Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](./lucide/index.md), [React](./react/index.md), [React Native](./react-native/index.md), [Vue](./vue/index.md), [Svelte](./svelte/index.md), [Preact](./preact/index.md), [Solid](./solid/index.md), [Angular](./angular/index.md), [Astro](./astro/index.md), and [Static](./static/index.md).
Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](https://lucide.dev/guide/packages/lucide), [React](https://lucide.dev/guide/packages/lucide-react), [React Native](https://lucide.dev/guide/packages/lucide-react-native), [Vue](https://lucide.dev/guide/packages/lucide-vue), [Vue 3](https://lucide.dev/guide/packages/lucide-vue-next), [Svelte](https://lucide.dev/guide/packages/lucide-svelte), [Preact](https://lucide.dev/guide/packages/lucide-preact), [Solid](https://lucide.dev/guide/packages/lucide-solid), [Angular](https://lucide.dev/guide/packages/angular), [Astro](https://lucide.dev/guide/packages/lucide-astro), and [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs).
## Community
+28 -64
View File
@@ -12,23 +12,19 @@ Implementation of the Lucide icon library for web applications.
::: code-group
```sh [pnpm]
pnpm add lucide
pnpm add lucide@next
```
```sh [yarn]
yarn add lucide
yarn add lucide@next
```
```sh [npm]
npm install lucide
npm install lucide@next
```
```sh [bun]
bun add lucide
```
```sh [deno]
deno add lucide
bun add lucide@next
```
:::
@@ -42,23 +38,19 @@ Implementation of the Lucide icon library for React applications.
::: code-group
```sh [pnpm]
pnpm add lucide-react
pnpm add lucide-react@next
```
```sh [yarn]
yarn add lucide-react
yarn add lucide-react@next
```
```sh [npm]
npm install lucide-react
npm install lucide-react@next
```
```sh [bun]
bun add lucide-react
```
```sh [deno]
deno add lucide-react
bun add lucide-react@next
```
:::
@@ -88,10 +80,6 @@ npm install @lucide/vue
bun add @lucide/vue
```
```sh [deno]
deno add @lucide/vue
```
:::
For more details, see the [documentation](./vue/index.md).
@@ -103,23 +91,19 @@ Implementation of the Lucide icon library for Svelte applications.
::: code-group
```sh [pnpm]
pnpm add @lucide/svelte
pnpm add @lucide/svelte@next
```
```sh [yarn]
yarn add @lucide/svelte
yarn add @lucide/svelte@next
```
```sh [npm]
npm install @lucide/svelte
npm install @lucide/svelte@next
```
```sh [bun]
bun add @lucide/svelte
```
```sh [deno]
deno add @lucide/svelte
bun add @lucide/svelte@next
```
:::
> `@lucide/svelte` is only for Svelte 5, for Svelte 4 use the `lucide-svelte` package.
@@ -133,23 +117,19 @@ Implementation of the Lucide icon library for Solid applications.
::: code-group
```sh [pnpm]
pnpm add lucide-solid
pnpm add lucide-solid@next
```
```sh [yarn]
yarn add lucide-solid
yarn add lucide-solid@next
```
```sh [npm]
npm install lucide-solid
npm install lucide-solid@next
```
```sh [bun]
bun add lucide-solid
```
```sh [deno]
deno add lucide-solid
bun add lucide-solid@next
```
:::
@@ -178,10 +158,6 @@ npm install @lucide/angular
bun add @lucide/angular
```
```sh [deno]
deno add @lucide/angular
```
:::
For more details, see the [documentation](./angular/index.md).
@@ -193,23 +169,19 @@ Implementation of the Lucide icon library for preact applications.
::: code-group
```sh [pnpm]
pnpm add lucide-preact
pnpm add lucide-preact@next
```
```sh [yarn]
yarn add lucide-preact
yarn add lucide-preact@next
```
```sh [npm]
npm install lucide-preact
npm install lucide-preact@next
```
```sh [bun]
bun add lucide-preact
```
```sh [deno]
deno add lucide-preact
bun add lucide-preact@next
```
@@ -224,23 +196,19 @@ Implementation of the Lucide icon library for Astro applications.
::: code-group
```sh [pnpm]
pnpm add @lucide/astro
pnpm add @lucide/astro@next
```
```sh [yarn]
yarn add @lucide/astro
yarn add @lucide/astro@next
```
```sh [npm]
npm install @lucide/astro
npm install @lucide/astro@next
```
```sh [bun]
bun add @lucide/astro
```
```sh [deno]
deno add @lucide/astro
bun add @lucide/astro@next
```
:::
@@ -254,23 +222,19 @@ Implementation of the Lucide icon library for multiple usages that like to use:
::: code-group
```sh [pnpm]
pnpm add lucide-static
pnpm add lucide-static@next
```
```sh [yarn]
yarn add lucide-static
yarn add lucide-static@next
```
```sh [npm]
npm install lucide-static
npm install lucide-static@next
```
```sh [bun]
bun add lucide-static
```
```sh [deno]
deno add lucide-static
bun add lucide-static@next
```
:::
+1 -10
View File
@@ -3,13 +3,12 @@
::: warning
This documentation is for `@lucide/angular`.
To learn about our legacy package for Angular, please refer to [`lucide-angular`](https://v0.lucide.dev/guide/packages/lucide-angular).
To learn about our legacy package for Angular, please refer to [`lucide-angular`](./lucide-angular).
:::
A standalone, signal-based, zoneless implementation of Lucide icons for Angular.
**What you can accomplish:**
- Use icons as standalone Angular components with full dependency injection support
- Configure icons globally through modern Angular providers
- Integrate with Angular's reactive forms and data binding
@@ -119,7 +118,6 @@ Alternatively, the component also accepts string inputs.
To use icons this way, first, you have to provide icons via `provideLucideIcons`:
:::code-group
```ts{7-10} [app.config.ts]
import { ApplicationConfig } from '@angular/core';
import { provideLucideIcons, LucideCircleCheck, LucideCircleX } from '@lucide/angular';
@@ -150,7 +148,6 @@ import { LucideIcon } from '@lucide/angular';
})
export class Foobar { }
```
:::
::: tip
@@ -262,25 +259,19 @@ export class Foobar {
## Troubleshooting
### The icon is not being displayed
If using per-icon-components:
1. Ensure that the icon component is being imported, if using per-icon-components
2. Check that the icon name matches exactly (case-sensitive)
If using the dynamic component:
1. Ensure the icon is provided via `provideLucideIcons()` if using string names
2. Verify the icon is imported from `@lucide/angular` and not the legacy package
### TypeScript errors?
Make sure you're importing from `@lucide/angular` and not `lucide-angular`.
### Icons render with wrong defaults
Ensure `provideLucideConfig()` is used at the right level.
## Migration guide
Migrating from `lucide-angular`? Read our [comprehensive migration guide](https://github.com/lucide-icons/lucide/blob/main/packages/angular/MIGRATION.md).
+187
View File
@@ -0,0 +1,187 @@
# Lucide Angular
::: warning
This documentation is for our legacy package for Angular.
For our modern, standalone-first implementation, please refer to [`@lucide/angular`](./angular).
:::
Angular components and services for Lucide icons that integrate with Angular's dependency injection and component system. Provides both traditional module-based and modern standalone component approaches for maximum flexibility in Angular applications.
**What you can accomplish:**
- Use icons as Angular components with full dependency injection support
- Configure icons globally through Angular services and providers
- Choose from multiple component selectors (lucide-angular, lucide-icon, i-lucide, span-lucide)
- Integrate with Angular's reactive forms and data binding
- Build scalable applications with tree-shaken icon bundles and lazy loading support
## Installation
::: code-group
```sh [pnpm]
pnpm add lucide-angular
```
```sh [yarn]
yarn add lucide-angular
```
```sh [npm]
npm install lucide-angular
```
```sh [bun]
bun add lucide-angular
```
:::
## How to use
### Step 1: Import `LucideAngularModule`
In any Angular module you wish to use Lucide icons in, you have to import `LucideAngularModule`, and pick any icons you wish to use:
```js
import { LucideAngularModule, File, House, Menu, UserCheck } from 'lucide-angular';
@NgModule({
imports: [
LucideAngularModule.pick({File, House, Menu, UserCheck})
]
})
export class AppModule { }
```
or using standalone version:
```js
import { Component } from '@angular/core';
import { LucideAngularModule, FileIcon } from 'lucide-angular';
@Component({
standalone: true,
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
imports: [LucideAngularModule]
})
export class AppComponent {
readonly FileIcon = FileIcon;
}
```
### Step 2: Use the icons in templates
Within your templates you may now use one of the following component tags to insert an icon:
```html
<lucide-angular name="file" class="my-icon"></lucide-angular>
<lucide-icon name="house" class="my-icon"></lucide-icon>
<i-lucide name="menu" class="my-icon"></i-lucide>
<span-lucide name="user-check" class="my-icon"></span-lucide>
```
for standalone
```html
<lucide-angular [img]="FileIcon" class="my-icon"></lucide-angular>
<lucide-icon [img]="FileIcon" class="my-icon"></lucide-icon>
<i-lucide [img]="FileIcon" class="my-icon"></i-lucide>
<span-lucide [img]="FileIcon" class="my-icon"></span-lucide>
```
### Props
You can pass additional props to adjust the icon appearance.
| name | type | default |
| --------------------- | --------- | ------------ |
| `size` | *number* | 24 |
| `color` | *string* | currentColor |
| `strokeWidth` | *number* | 2 |
| `absoluteStrokeWidth` | *boolean* | false |
```html
<i-lucide name="house" [size]="48" color="red" [strokeWidth]="1"></i-lucide>
```
### Global configuration
You can inject the `LucideIconConfig` service in your root component to globally configure the default property values as defined above.
### Styling using a custom CSS class
Any extra HTML attribute is ignored, but the `class` attribute
is passed onto the internal SVG image element and it can be used to style it:
```css
svg.my-icon {
width: 12px;
height: 12px;
stroke-width: 3;
}
```
## Injecting multiple icon providers
You may provide additional icons using the `LUCIDE_ICONS` injection token,
which accepts multiple providers of the interface `LucideIconsProviderInterface`
with the utility class `LucideIconsProvider` available for easier usage:
```js
import { LUCIDE_ICONS, LucideIconProvider } from 'lucide-angular';
import { MyIcon } from './icons/my-icon';
const myIcons = {MyIcon};
@NgModule({
providers: [
{provide: LUCIDE_ICONS, multi: true, useValue: new LucideIconProvider(myIcons)},
]
})
export class AppModule { }
```
To add custom icons, you will first need to convert them to an [svgson format](https://github.com/elrumordelaluz/svgson).
## Loading all icons
::: danger
You may also opt to import all icons if necessary using the following format but be aware that this will significantly increase your application build size.
:::
```js
import { icons } from 'lucide-angular';
...
LucideAngularModule.pick(icons)
```
## With Lucide Lab or custom icons
[Lucide Lab](https://github.com/lucide-icons/lucide-lab) is a collection of icons that are not part of the Lucide main library.
They can be used in the same way as the official icons.
```js
import { LucideAngularModule } from 'lucide-angular';
import { coconut } from '@lucide/lab';
@NgModule({
imports: [
LucideAngularModule.pick({ coconut })
]
})
export class AppModule { }
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```html
<lucide-icon name="check" aria-label="Task completed"></lucide-icon>
```
For best practices on accessibility, please see our [accessibility guide](../accessibility.md).
+1 -2
View File
@@ -3,7 +3,6 @@
Vue 2 components for Lucide icons that integrate with Vue's Options API and template system. Each icon is a Vue component that renders as an inline SVG, providing familiar Vue development patterns for legacy applications still using Vue 2.
**What you can accomplish:**
- Use icons as Vue 2 components with Options API integration
- Maintain legacy Vue 2 applications with modern icon components
- Integrate with Vue 2's template system and component lifecycle
@@ -156,7 +155,7 @@ You can add accessibility attributes using aria-labels.
```vue
<script setup>
import { Check } from '@lucide/vue';
import { Check } from 'lucide-vue-next';
</script>
<template>
@@ -9,7 +9,7 @@ import Sandpack from '~/.vitepress/theme/components/editors/SandpackPreact.vue'
# Combining icons
You can combine multiple icons into a single icon by nesting SVG elements.
This is useful if you want to create custom icons by combining existing ones.
This is useful if you want to create custom icons icons by combining existing ones.
::: sandpack {showTabs=false editorHeight=400 editorWidthPercentage=60 dependencies="lucide-preact"}
@@ -6,7 +6,7 @@ description: Learn how to combine multiple icons into a single icon nested SVG e
# Combining icons
You can combine multiple icons into a single icon by nesting SVG elements.
This is useful if you want to create custom icons by combining existing ones.
This is useful if you want to create custom icons icons by combining existing ones.
```SnackPlayer name=State&ext=js&dependencies=react-native-svg,lucide-react-native
import React, {useState, useEffect} from 'react';
@@ -1,56 +0,0 @@
---
title: Optimizations - React Native
description: Learn how to optimize the bundle size of your React Native and Expo web exports by tree-shaking Lucide icons.
---
# Optimizations
By default you import icons from the `lucide-react-native` entry point:
```jsx
import { Camera } from 'lucide-react-native';
```
This is convenient, but it relies on the bundler to _tree-shake_ the entry point (a "barrel" file that re-exports every icon) so that only the icons you actually use end up in your bundle.
On native platforms this is usually fine. On **web exports** it can be a problem, because the Metro bundler is currently not very good at tree-shaking barrel imports and may include **all** icons in your final bundle.
## The Metro tree-shaking limitation
When you build for web with Expo (`expo export --platform web`), Metro is used to bundle your JavaScript. Metro bundles modules on-demand and, by default, does not remove unused exports from a barrel file.
This means an import like the one below can pull in the entire icon set, even though only a single icon is rendered:
```jsx
// May bundle every Lucide icon on web exports
import { Camera } from 'lucide-react-native';
```
Expo documents an experimental optimization for [React Native web imports](https://docs.expo.dev/guides/tree-shaking/#react-native-web-imports) and for [removing unused imports and exports](https://docs.expo.dev/guides/tree-shaking/#remove-unused-imports-and-exports). In practice this tree-shaking does **not** currently work reliably for `lucide-react-native` barrel imports, so the unused icons are not removed from web exports.
## Recommended: import icons individually
To guarantee that only the icons you use are bundled — regardless of the bundler's tree-shaking capabilities — import each icon directly from its own module:
```jsx
import Camera from 'lucide-react-native/icons/camera';
// Usage
const App = () => {
return <Camera />;
};
export default App;
```
Because each icon lives in its own file, the bundler only ever includes the icons you explicitly import. This keeps your web export small without depending on Metro's experimental tree-shaking.
The icon module name is the [kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) version of the icon name. For example, the `ArrowRight` icon is imported from `lucide-react-native/icons/arrow-right`:
```jsx
import ArrowRight from 'lucide-react-native/icons/arrow-right';
```
::: tip
Individual icon imports work identically on native and web, so you can safely use this style across your whole project to keep bundles as small as possible on every platform.
:::
+1 -1
View File
@@ -9,7 +9,7 @@ import Sandpack from '~/.vitepress/theme/components/editors/Sandpack.vue'
# Combining icons
You can combine multiple icons into a single icon by nesting SVG elements.
This is useful if you want to create custom icons by combining existing ones.
This is useful if you want to create custom icons icons by combining existing ones.
::: sandpack {template=react showTabs=false editorHeight=400 editorWidthPercentage=60 dependencies="lucide-react"}
+1 -1
View File
@@ -5,7 +5,7 @@ description: Learn about the different types exported by the `lucide-react` pack
# TypeScript Support
List of exported types from the `lucide-react` package.
These can be used to type your components when using Lucide icons in a TypeScript React project.
These can be used to type your components when using Lucide icons in a TypeScript React project
## `LucideProps`
+1 -1
View File
@@ -10,7 +10,7 @@ import Sandpack from '~/.vitepress/theme/components/editors/Sandpack.vue'
# Combining icons
You can combine multiple icons into a single icon by nesting SVG elements.
This is useful if you want to create custom icons by combining existing ones.
This is useful if you want to create custom icons icons by combining existing ones.
::: sandpack {template=vite-solid showTabs=false editorHeight=400 editorWidthPercentage=60 dependencies="lucide-solid"}
@@ -9,7 +9,7 @@ import Sandpack from '~/.vitepress/theme/components/editors/SandpackSvelte.vue';
# Combining icons
You can combine multiple icons into a single icon by nesting SVG elements.
This is useful if you want to create custom icons by combining existing ones.
This is useful if you want to create custom icons icons by combining existing ones.
::: sandpack {template=vite-svelte showTabs=false editorHeight=400 editorWidthPercentage=60}
+6 -4
View File
@@ -15,22 +15,24 @@ Make sure you have a Svelte environment set up. If you don't have one yet, you c
## Installation
<!-- TODO: v1 Remove After full release -->
::: code-group
```sh [pnpm]
pnpm install @lucide/svelte
pnpm install @lucide/svelte@next
```
```sh [yarn]
yarn add @lucide/svelte
yarn add @lucide/svelte@next
```
```sh [npm]
npm install @lucide/svelte
npm install @lucide/svelte@next
```
```sh [bun]
bun add @lucide/svelte
bun add @lucide/svelte@next
```
:::
+1 -1
View File
@@ -9,7 +9,7 @@ import Sandpack from '~/.vitepress/theme/components/editors/SandpackVue.vue'
# Combining icons
You can combine multiple icons into a single icon by nesting SVG elements.
This is useful if you want to create custom icons by combining existing ones.
This is useful if you want to create custom icons icons by combining existing ones.
::: sandpack {template=vue showTabs=false editorHeight=400 editorWidthPercentage=60 dependencies="@lucide/vue"}
+2 -12
View File
@@ -1,19 +1,9 @@
import * as iconDetails from '../.vitepress/data/iconDetails';
import iconNodes from '../.vitepress/data/iconNodes';
export default {
async load() {
return {
icons: Object.entries(iconDetails).map(
([, { name, iconNode, popularity, createdRelease, aliases = [] }]) => ({
name,
iconNode,
popularity: popularity?.count ?? 0,
aliases: aliases
.map((alias) => (typeof alias === 'string' ? alias : alias?.name))
.filter((alias): alias is string => Boolean(alias)),
createdRelease,
}),
),
icons: Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode })),
};
},
};
+28 -30
View File
@@ -9,15 +9,14 @@
"docs:build": "pnpm run /^prebuild:.*/ && vitepress build",
"docs:preview": "vitepress preview",
"build:docs": "vitepress build",
"prebuild:iconNodes": "node ./scripts/writeIconNodes.mts",
"prebuild:metaJson": "node ./scripts/writeIconMetaIndex.mts",
"prebuild:releaseJson": "node ./scripts/writeReleaseMetadata.mts",
"prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mts",
"prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mts",
"prebuild:iconDetails": "node ./scripts/writeIconDetails.mts",
"prebuild:iconPopularity": "node ./scripts/writeiconPopularity.mts",
"prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mts",
"postbuild:vercelJson": "node ./scripts/writeVercelOutput.mts",
"prebuild:iconNodes": "node ./scripts/writeIconNodes.mjs",
"prebuild:metaJson": "node ./scripts/writeIconMetaIndex.mjs",
"prebuild:releaseJson": "node ./scripts/writeReleaseMetadata.mjs",
"prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mjs",
"prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mjs",
"prebuild:iconDetails": "node ./scripts/writeIconDetails.mjs",
"prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mjs",
"postbuild:vercelJson": "node ./scripts/writeVercelOutput.mjs",
"dev": "npx nitropack dev",
"prebuild:api": "npx nitropack prepare",
"build:api": "npx nitropack build",
@@ -32,40 +31,39 @@
"@lucide/shared": "workspace:*",
"@rollup/plugin-replace": "^6.0.3",
"@types/semver": "^7.7.1",
"h3": "^1.15.11",
"markdown-it": "^14.3.0",
"markdown-it-container": "^4.0.0",
"nitropack": "^2.13.4",
"h3": "^1.15.9",
"nitropack": "^2.12.4",
"rollup-plugin-copy": "^3.5.0",
"semver": "^7.8.5",
"sharp": "^0.34.5",
"shiki": "^3.23.0",
"simple-git": "^3.36.0",
"sitemap": "^7.1.3",
"svg-path-commander": "^2.2.1",
"svg-pathdata": "^6.0.3",
"svgson": "^5.3.1",
"svg-path-commander": "^2.1.11",
"vitepress": "^1.6.4",
"vitepress-plugin-group-icons": "^1.7.5",
"vitepress-plugin-llms": "^1.13.3"
"vitepress-plugin-group-icons": "^1.6.5",
"vitepress-plugin-llms": "^1.11.1",
"markdown-it": "^14.1.0",
"markdown-it-container": "^4.0.0",
"semver": "^7.7.3",
"sharp": "^0.34.5",
"shiki": "^3.15.0",
"simple-git": "^3.32.3",
"sitemap": "^7.1.2",
"svg-pathdata": "^6.0.3",
"svgson": "^5.3.1"
},
"dependencies": {
"@floating-ui/vue": "^1.1.11",
"@floating-ui/vue": "^1.1.9",
"@headlessui/vue": "^1.7.23",
"@lucide/vue": "workspace:*",
"@vueuse/components": "^14.3.0",
"@vueuse/core": "^14.3.0",
"@vueuse/integrations": "^14.3.0",
"@vueuse/components": "^14.0.0",
"@vueuse/core": "^14.0.0",
"element-to-path": "^1.2.1",
"fuse.js": "^7.5.0",
"fuse.js": "^7.1.0",
"jszip": "^3.10.1",
"lodash-es": "^4.18.1",
"lodash-es": "^4.17.23",
"lucide": "workspace:*",
"lucide-react": "workspace:*",
"motion-v": "1.10.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"sandpack-vue3": "3.1.12",
"vue": "^3.5.39"
"vue": "^3.5.24"
}
}
-6
View File
@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 200" fill="none">
<path fill="#00e8ff" d="M40 120l20-60l90 90l-30 50l-40-40h-20" />
<path fill="#283198" d="M80 160 L80 80 L120 40 L 120 120 M0 160 L40 200 L40 120 L20 120" />
<path fill="#324fff" d="M40 120v-80l40-40v80M120 200v-80l40-40v80M0 160v-80l40 40" />
<path fill="#0ff" d="M40 200v-80l40 40" />
</svg>

Before

Width:  |  Height:  |  Size: 377 B

-15
View File
@@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="138" height="42" fill="none" viewBox="0 0 138 42">
<path fill="#fff" fill-rule="evenodd" d="M138 4a4 4 0 0 0-4-4H4a4 4 0 0 0-4 4v34a4 4 0 0 0 4 4h130a4 4 0 0 0 4-4z" clip-rule="evenodd"/>
<path fill="url(#a)" fill-rule="evenodd" d="M16 27.6a6.6 6.6 0 0 0 5.5-10.4l4.3-4.3c.3-.3.1-1-.4-1a29 29 0 0 1-4.8-1.4h-.1l-.6-.3A11.4 11.4 0 0 0 5.1 24.5c.2.6 1 .9 1.5.6q.7-.6.5-1.4a9.3 9.3 0 0 1 14.6-10l-2 1.8q-1.6-1-3.7-1.1a6.6 6.6 0 0 0-5.4 10.4L6.3 29c-.4.3-.2 1 .3 1a29 29 0 0 1 4.8 1.4h.1l.6.3a11 11 0 0 0 12-2.7 11 11 0 0 0 2.8-11.6c-.2-.6-1-.9-1.5-.6q-.6.6-.5 1.4a9.3 9.3 0 0 1-14.6 10l2-1.8q1.6 1 3.7 1.1m4.4-6.6a4.5 4.5 0 0 1-6.7 4l6.2-6.1q.5.9.5 2.1m-9 0a4.5 4.5 0 0 1 6.8-3.9L12 23.3q-.6-1-.6-2.3" clip-rule="evenodd"/>
<path fill="url(#b)" d="M57.7 19.3q3.5.1 3.5 4 0 3.6-3.5 3.8-2.3 0-2.8-1.9h-.3l.2 1.4v3.7h-1.4V19.5h1.4l-.2 1.7h.3q.6-1.9 2.8-1.9m16.6 7.4h.3l2-7.2h1.5l-2.5 8.3q-.4 2-2.4 2.2l-2.2.2v-1.4h2.5q.5.1.5-.4l.5-1.5H73l-2.2-7.4h1.4zm-26-7.4q3.3 0 3.3 3.7H46c0 1.7.5 3 2.5 3q2 0 2-2.1H52q-.4 3.1-3.5 3.2-3.8-.1-3.9-3.9 0-3.7 3.7-3.9m46.1-3q4 0 4.4 4.2h-1.5q-.2-3-3-3-3 0-3 4.2t3.3 4.2q3 0 3.2-3h1.4q-.3 4.3-4.6 4.2-4.7 0-4.7-5.4t4.5-5.4m16 3q3.6.1 3.8 4-.2 3.6-3.9 3.8-3.7-.1-3.8-3.9.1-3.7 3.8-3.9m6.4 4.6q0 2 2 2 2.3-.2 2.3-2.4v-4h1.4v5.8q-.1.6.4.4l.9-.1v1.3h-.8q-1.6 0-1.7-1.6h-.3q-.5 1.8-2.6 1.8-2.9 0-2.8-3v-4.6h1.5zm15.6 1.4q-.1.6.4.4l.9-.1v1.3h-.8q-1.7-.1-1.8-1.7h-.3q-.4 1.9-2.8 2-3.4-.2-3.5-4 .1-3.7 3.5-3.9 2.4 0 2.8 2h.3l-.1-1.4v-4h1.4zm-89-5.7L36 25.4v.3h7.8v1.2h-8v-3l7.5-5.8v-.3H36v-1.2h7.4zm21 .3L64 21h.3q.5-1.8 2.7-1.8 2.8 0 2.7 3v4.6h-1.5l.3-4.4q0-2-2-2-2.3.1-2.3 2.5v4H63V15.7h1.4zM80.5 21h.3q.4-1.7 2.2-1.6h1.3V21l-1.6-.2q-2 0-2 2.3v4h-1.5v-7.5h1.4zm20.9-5.3q2 0 2 2v8h2.2v1.1h-6v-1.2h2.4v-8.4q.1-.6-.5-.4l-1.4.3v-1.4zm8.8 4.7q-2.4 0-2.4 2.7t2.4 2.7q2.4 0 2.5-2.7 0-2.6-2.5-2.7m-52.8 0q-2.6 0-2.6 2.7.1 2.6 2.6 2.7 2.3 0 2.3-2.7 0-2.6-2.3-2.7m70.8 0q-2.4 0-2.4 2.7 0 2.6 2.4 2.7 2.7 0 2.6-2.7-.1-2.6-2.6-2.7m-80 0q-1.8 0-2.1 1.5h4q0-1.4-1.9-1.5"/>
<defs>
<linearGradient id="a" x1="4.6" x2="27.4" y1="21" y2="21" gradientUnits="userSpaceOnUse">
<stop stop-color="#010101"/>
<stop offset="1" stop-color="#949494"/>
</linearGradient>
<linearGradient id="b" x1="35.7" x2="133.7" y1="23.1" y2="23.1" gradientUnits="userSpaceOnUse">
<stop stop-color="#010101"/>
<stop offset="1" stop-color="#646464"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

@@ -26,7 +26,6 @@ const writeIconFiles = icons.map(async (iconFileName) => {
import iconNode from '../iconNodes/${iconName}.node.json'
import metaData from '../../../../icons/${iconName}.json'
import releaseData from '../releaseMetadata/${iconName}.json'
import popularity from '../iconPopularity/${iconName}.json'
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData
@@ -38,7 +37,6 @@ const iconDetails = {
categories,
aliases,
deprecated,
popularity,
deprecationReason,
toBeRemovedInVersion,
...releaseData,
@@ -42,7 +42,7 @@ const getRelatedIcons = (currentIcon, icons) => {
};
const iconsMetaDataPromises = svgFiles.map(async (iconName) => {
const metaDataFileContent = await fs.promises.readFile(`../icons/${iconName}`, 'utf-8');
const metaDataFileContent = await fs.promises.readFile(`../icons/${iconName}`);
const metaData = JSON.parse(metaDataFileContent);
const name = iconName.replace('.json', '');
@@ -70,10 +70,10 @@ const comparisonsPromises = tags.map(async (tag, index) => {
};
});
const comparisons = (await Promise.all(comparisonsPromises)).filter((comparison) => comparison != null && Object.keys(comparison.iconFiles).length > 0);
const comparisons = await Promise.all(comparisonsPromises);
const newReleaseMetaData = {};
comparisons.forEach(({ tag, iconFiles, date }) => {
comparisons.forEach(({ tag, iconFiles, date } = {}) => {
if (tag == null) return;
iconFiles.forEach(({ status, file, renamedFile }) => {
@@ -11,7 +11,7 @@ const iconMetaData = await getIconMetaData(path.resolve(scriptDir, '../../icons'
const iconAliasesRedirectRoutes = Object.entries(iconMetaData)
.filter(([, { aliases }]) => aliases?.length)
.map(([iconName, { aliases }]) => {
const aliasRouteMatches = aliases.map((alias) => typeof alias === 'string' ? alias : alias?.name).join('|');
const aliasRouteMatches = aliases.map((alias) => alias.name).join('|');
return {
src: `^/icons/(${aliasRouteMatches})$`,
-104
View File
@@ -1,104 +0,0 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import fs from 'fs';
import path from 'path';
import { loadEnvFile } from 'node:process';
import { readSvgDirectory } from '@lucide/helpers';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../icons');
const iconPopularityDirectory = path.resolve(currentDir, '.vitepress/data', 'iconPopularity');
try {
// Load environment variables from .env file, if it exists.
loadEnvFile(`${currentDir}/.env`);
} catch (error) {}
if (fs.existsSync(iconPopularityDirectory)) {
fs.rmSync(iconPopularityDirectory, { recursive: true, force: true });
}
if (!fs.existsSync(iconPopularityDirectory)) {
fs.mkdirSync(iconPopularityDirectory);
}
const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json');
const iconNames = iconJsonFiles.map((file) => path.basename(file, '.json'));
const defaultIconNamePopularity: Record<string, number> = Object.fromEntries(
iconNames.map((iconName) => [iconName, 0])
);
interface AnalyticsResponse {
results: {
dimensions: [route: string];
metrics: [count: number];
}[];
}
const writeIconPopularity = async (iconVisitCounts: Record<string, number>) => {
const writePopularityPromises = Object.entries(iconVisitCounts).map(([iconName, count]) => {
const location = path.resolve(iconPopularityDirectory, `${iconName}.json`);
return fs.promises.writeFile(location, JSON.stringify({ count }, null, 2), 'utf-8');
});
await Promise.all(writePopularityPromises);
console.log('Successfully written icon popularity data for', writePopularityPromises.length, 'icons.');
};
if (process.env.LUCIDE_ANALYTICS_TOKEN === undefined) {
console.warn('LUCIDE_ANALYTICS_TOKEN environment variable is not set. Writing default icon popularity data.');
await writeIconPopularity(defaultIconNamePopularity);
process.exit(0);
}
try {
const response = await fetch('https://analytics.lucide.dev/api/v2/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.LUCIDE_ANALYTICS_TOKEN}`,
},
body: JSON.stringify({
"site_id": "lucide.dev",
"metrics": [
"visitors"
],
"dimensions": [
"visit:entry_page"
],
"date_range": "12mo"
}),
})
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const analyticsData: AnalyticsResponse = await response.json();
const iconVisitCounts = analyticsData.results.reduce((acc, { dimensions, metrics }) => {
const [route] = dimensions;
const [count] = metrics;
if (!route.startsWith('/icons')) {
return acc
}
// Only match routes that start with /icons/ and end with an icon name, e.g. /icons/activity, /icons/activity/ or /icons/activity?ref=homepage
const match = route.match(/^\/icons\/([^\/\?]+)(?:[\/\?]|$)/);
if (match) {
const iconName = match[1];
if (iconNames.includes(iconName)) {
acc[iconName] = (acc[iconName] || 0) + count;
}
}
return acc;
}, defaultIconNamePopularity);
await writeIconPopularity(iconVisitCounts);
} catch (error) {
console.error('Error fetching analytics data:', error);
await writeIconPopularity(defaultIconNamePopularity);
}
+3 -11
View File
@@ -13,7 +13,7 @@
"title": "Lucide Icons icon schema",
"type": "object",
"additionalProperties": false,
"required": ["$schema", "contributors", "categories", "tags", "use-cases"],
"required": ["$schema", "categories", "tags"],
"properties": {
"$schema": {
"type": "string"
@@ -40,7 +40,7 @@
}
},
"dependentRequired": {
"deprecated": ["deprecationReason"],
"deprecated": ["deprecationReason", "toBeRemovedInVersion"],
"deprecationReason": ["deprecated"],
"toBeRemovedInVersion": ["deprecated"]
}
@@ -114,14 +114,6 @@
"minItems": 1,
"uniqueItems": true
},
"use-cases": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 0,
"uniqueItems": true
},
"deprecated": {
"const": true
},
@@ -134,7 +126,7 @@
}
},
"dependentRequired": {
"deprecated": ["deprecationReason"],
"deprecated": ["deprecationReason", "toBeRemovedInVersion"],
"deprecationReason": ["deprecated"],
"toBeRemovedInVersion": ["deprecated"]
},
-3
View File
@@ -6,9 +6,6 @@
"danielbayley",
"ericfennis"
],
"use-cases": [
"Decreasing the font size in a text editor"
],
"tags": [
"letter",
"font size",
-3
View File
@@ -6,9 +6,6 @@
"danielbayley",
"ericfennis"
],
"use-cases": [
"Increasing the font size in a text editor"
],
"tags": [
"letter",
"font size",
-5
View File
@@ -1,10 +1,5 @@
{
"$schema": "../icon.schema.json",
"use-cases": [
"indicating a large and small text size",
"formatting text size",
"indicating a heading and body text"
],
"contributors": [
"it-is-not",
"jguddas",
-4
View File
@@ -4,10 +4,6 @@
"karsa-mistmere",
"jguddas"
],
"use-cases": [
"Representing physical disability or accessibility assistance",
"Marking wheelchair-accessible facilities or routes"
],
"tags": [
"disability",
"disabled",
-5
View File
@@ -4,11 +4,6 @@
"colebemis",
"jguddas"
],
"use-cases": [
"Representing an activity feed or recent actions",
"Showing health metrics such as heart rate or vital signs",
"Indicating live system activity or monitoring"
],
"tags": [
"pulse",
"action",
-30
View File
@@ -1,30 +0,0 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"ahtohbi4",
"jamiemlaw",
"karsa-mistmere",
"jguddas"
],
"use-cases": [
"marking content as sponsored",
"linking to an advertise with us page"
],
"tags": [
"advert",
"affiliate",
"brand",
"campaign",
"commercial",
"marketing",
"monetize",
"paid",
"partner",
"promo",
"sponsor"
],
"categories": [
"multimedia",
"notifications"
]
}
-16
View File
@@ -1,16 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13H6" />
<path d="M10 15v-4a2 2 0 0 0-4 0v4" />
<path d="M14 14.5a.5.5 0 0 0 .5.5h1a2.5 2.5 0 0 0 2.5-2.5v-1A2.5 2.5 0 0 0 15.5 9h-1a.5.5 0 0 0-.5.5z" />
<rect x="2" y="5" width="20" height="14" rx="2" />
</svg>

Before

Width:  |  Height:  |  Size: 434 B

-4
View File
@@ -4,10 +4,6 @@
"karsa-mistmere",
"jguddas"
],
"use-cases": [
"Representing air conditioning or ventilation in smart home apps",
"Indicating climate control amenities on hotel and booking platforms"
],
"tags": [
"air conditioner",
"ac",
-4
View File
@@ -5,10 +5,6 @@
"csandman",
"ericfennis"
],
"use-cases": [
"Streaming or mirroring a screen to an external display",
"Casting media to a TV or compatible device"
],
"tags": [
"stream",
"cast",
+2 -5
View File
@@ -6,10 +6,6 @@
"jguddas",
"karsa-mistmere"
],
"use-cases": [
"Confirming an alarm or scheduled reminder is set",
"Marking a timed task as complete"
],
"tags": [
"done",
"todo",
@@ -26,7 +22,8 @@
{
"name": "alarm-check",
"deprecationReason": "alias.name",
"deprecated": true
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
]
}
+2 -4
View File
@@ -6,9 +6,6 @@
"jguddas",
"karsa-mistmere"
],
"use-cases": [
"Removing or deleting an alarm"
],
"tags": [
"remove"
],
@@ -21,7 +18,8 @@
{
"name": "alarm-minus",
"deprecationReason": "alias.name",
"deprecated": true
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
]
}
-3
View File
@@ -6,9 +6,6 @@
"karsa-mistmere",
"jguddas"
],
"use-cases": [
"Turning off or disabling an alarm"
],
"tags": [
"morning",
"turn-off"

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