Compare commits

..

10 Commits

Author SHA1 Message Date
Eric Fennis
ba3d66dc53 Merge branch 'main' of https://github.com/lucide-icons/lucide into backwards-compatible-classnames 2025-05-02 15:44:38 +02:00
Eric Fennis
c663a141b6 Update snapshots 2025-04-04 16:27:24 +02:00
Eric Fennis
35a1ec15cc Merge branch 'main' of https://github.com/lucide-icons/lucide into backwards-compatible-classnames 2025-04-04 14:46:42 +02:00
Eric Fennis
8a44e63e80 Adjust classes lucide vue next 2025-03-22 11:26:25 +01:00
Eric Fennis
35afa59c99 Add support for icon alias classnames in lucide-svelte 2025-03-21 11:18:03 +01:00
Eric Fennis
c42676b0fd Add support for alias classNames in lucide-static 2025-03-21 10:56:54 +01:00
Eric Fennis
613cac84d0 Add support for alias classnames in lucide-solid 2025-03-19 11:38:26 +01:00
Eric Fennis
cf48dbb2d3 Add support for preact 2025-03-19 11:14:07 +01:00
Eric Fennis
4edcf24fc9 Support aliased class names in lucide-react 2025-03-19 10:58:24 +01:00
Eric Fennis
1efc9eb8b5 Add support for aliases names in export template 2025-03-19 10:51:36 +01:00
1244 changed files with 9998 additions and 20014 deletions

View File

@@ -8,6 +8,7 @@ squircle
strikethrough
touchpad
ungroup
pilcrow
toc
# Brands

View File

@@ -7,10 +7,8 @@ body:
value: |
Before submitting an icon request check if it has already been requested. If there is an open request, please add a 👍.
> [!CAUTION]
> New brand logos are **not** allowed, see our official statement: https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md.
>
> Existing brand icons are being phased out. Consider using https://simpleicons.org, which offers purpose-built SVGs that are also on a 24×24px grid.
**Important note**: No new brand logos are allowed, see https://github.com/lucide-icons/lucide/issues/670.
Existing brand icons will also be phased out. For brand icons, consider using https://simpleicons.org, which offers purpose-built SVGs that are also on a 24×24px grid.
- type: input
id: name
attributes:
@@ -43,9 +41,9 @@ body:
required: true
- label: I have searched existing icons to make sure it does not already exist and I didn't find any.
required: true
- label: I am not requesting a brand logo and the art is not protected by copyright, see more at https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md
- label: I am not requesting a brand logo and the art is not protected by copyright.
required: true
- label: I am not requesting an icon that includes religious, war/violence related, political imagery or hate symbols.
- label: I am not requesting an icon that includes religious, political imagery or hate symbols.
required: true
- label: I have provided appropriate use cases for the icon(s) requested.
required: true

View File

@@ -25,12 +25,10 @@ Common scopes: icons, docs, studio, site, dev
### Concept <!-- ONLY for new icons -->
<!-- All of these requirements must be fulfilled. -->
<!-- IMPORTANT! Please read our official statement on brand logos in Lucide: -->
<!-- https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md -->
- [ ] I have provided valid use cases for each icon.
- [ ] I have [not added any brand or logo icon](https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md).
- [ ] I have not added any a brand or logo icon.
- [ ] I have not used any hate symbols.
- [ ] I have not included any religious, war/violence related or political imagery.
- [ ] I have not included any religious or political imagery.
### Author, credits & license<!-- ONLY for new icons. -->
<!-- Please choose one of the following, and put an "x" next to it. -->

View File

@@ -7,10 +7,6 @@ on:
paths:
- icons/**/*.svg
permissions:
id-token: write # Required for OIDC
contents: write
jobs:
create-release:
if: github.repository == 'lucide-icons/lucide' && startsWith(github.event.head_commit.message, 'feat(icons)')
@@ -19,9 +15,9 @@ jobs:
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -36,19 +32,25 @@ jobs:
id: latest-tag
run: echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT
- name: Log latest tag
run: echo '${{ steps.latest-tag.outputs.LATEST_TAG }}'
- name: Check if we can patch
run: pnpm semver $LATEST_TAG -i minor
env:
LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }}
run: .github/workflows/version-up.sh --minor
- name: Create new version
id: new-version
run: echo "NEW_VERSION=$(pnpm semver $LATEST_TAG -i minor)" >> $GITHUB_OUTPUT
run: echo "NEW_VERSION=$(.github/workflows/version-up.sh --minor)" >> $GITHUB_OUTPUT
- name: Create change log
id: change-log
run: |
CHANGE_LOG=$(pnpm run generate:changelog --old-tag=${{ steps.latest-tag.outputs.LATEST_TAG }})
CHANGE_LOG=$(tail -n +5 <<< $CHANGE_LOG)
echo $CHANGE_LOG
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "CHANGE_LOG<<$EOF" >> $GITHUB_OUTPUT
echo "$CHANGE_LOG" >> $GITHUB_OUTPUT
echo "$EOF" >> $GITHUB_OUTPUT
env:
LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }}
GITHUB_API_KEY: ${{ secrets.GITHUB_TOKEN }}
- name: Check output
run: |
@@ -62,6 +64,38 @@ jobs:
name: Version ${{ steps.new-version.outputs.NEW_VERSION }}
generate_release_notes: true
test-semantic-release:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Semantic Release
id: semantic
uses: cycjimmy/semantic-release-action@v4
with:
tag_format: ${version}
branches: |
['new-release-workflow']
extends: |
semantic-release-monorepo
extra_plugins: |
@semantic-release/github
@semantic-release/git
@semantic-release/release-notes-generator
conventional-changelog-conventionalcommits
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Log output
if: steps.semantic.outputs.new_release_published == 'true'
run: |
echo ${{ steps.semantic.outputs.new_release_version }}
echo ${{ steps.semantic.outputs.new_release_major_version }}
echo ${{ steps.semantic.outputs.new_release_minor_version }}
echo ${{ steps.semantic.outputs.new_release_patch_version }}
start-release:
if: github.repository == 'lucide-icons/lucide'
needs: create-release

View File

@@ -1,4 +1,4 @@
name: Close Icon Requests with Brand Terms
name: Close Issue with Banned Phrases
on:
issues:
@@ -11,38 +11,25 @@ jobs:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Load stopwords from JSON & check issue title & body
if: contains(github.event.issue.labels.*.name, '🙌 icon request')
- name: Check for blocked phrases in issue title
run: |
ISSUE_TITLE=$(jq -r '.issue.title' "$GITHUB_EVENT_PATH")
ISSUE_BODY=$(jq -r '.issue.body // ""' "$GITHUB_EVENT_PATH")
ICON_NAME_SECTION=$(printf '%s\n' "$ISSUE_BODY" | awk '/### Icon name/{flag=1; next} /^### /{flag=0} flag')
BLOCKED_PHRASES=("twitter" "whatsapp" "logo" "google" "tiktok" "facebook" "slack" "discord" "bluesky" "spotify" "behance" "pix" "x.com")
jq -r 'to_entries[] | "\(.key) \(.value)"' brand-stopwords.json | while read -r KEY VALUE; do
SAFE_KEY=$(printf '%s\n' "$KEY" | sed 's/[][\.^$*]/\\&/g')
SAFE_VALUE=$(printf '%s\n' "$VALUE" | sed 's/[][\.^$*]/\\&/g')
# Check title and body for blocked phrases
for PHRASE in "${BLOCKED_PHRASES[@]}"
do
if echo "$ISSUE_TITLE" | grep -i "$PHRASE"; then
gh issue close ${{ github.event.issue.number }} --reason "not planned" --comment "This looks like a duplicate, use the [search](https://github.com/lucide-icons/lucide/issues?q=is%3Aissue+$PHRASE) to find similar issues.
if echo "$ISSUE_TITLE" | grep -iqE "$SAFE_KEY|$SAFE_VALUE" || \
{ [ -n "$ICON_NAME_SECTION" ] && echo "$ICON_NAME_SECTION" | grep -iqE "$SAFE_KEY|$SAFE_VALUE"; }; then
gh issue close ${{ github.event.issue.number }} \
--reason "not_planned" \
--comment "It looks like this request is about **${VALUE}**, which is a brand logo.
Lucide **does not accept** brand logos, and we do not plan to add them in the future. This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
[Click here to read our official statement about brand logos in Lucide.](./BRAND_LOGOS_STATEMENT.md)
You can [search for similar issues.](https://github.com/lucide-icons/lucide/issues?q=is%3Aissue+${VALUE})
Were always happy to help on [Discord](https://discord.gg/EH6nSts)."
Read more about brand guideline rules at [We're not accepting new Brand icons #670](https://github.com/lucide-icons/lucide/issues/670).
Always happy to help on [Discord](https://discord.gg/EH6nSts)."
gh issue lock ${{ github.event.issue.number }} --reason spam
exit 0
exit 1
fi
done
env:
GH_TOKEN: ${{ github.token }}
env:
GH_TOKEN: ${{ github.token }}

View File

@@ -9,5 +9,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/labeler@v5

View File

@@ -9,9 +9,9 @@ jobs:
lint-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,10 +13,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: 'package.json'
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
@@ -24,7 +21,7 @@ jobs:
files: icons/*
- name: Generate annotations
run: node ./scripts/lintFilenames.mts
run: node ./scripts/lintFilenames.mjs
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
@@ -34,6 +31,6 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Check Uniqueness of Aliases
run: "! cat <(printf \"%s\\n\" icons/*.json | while read -r name; do basename \"$name\" .json; done) <(jq -cr 'select(.aliases) | .aliases[] | if type==\"string\" then . else .name end' icons/*.json) | sort | uniq -c | grep -ve '^\\s*1 '"

View File

@@ -11,9 +11,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -27,9 +27,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -12,11 +12,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
node-version: 18
cache: 'pnpm'
- name: Install dependencies
@@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3.8.1
with:
node-version-file: 'package.json'
node-version: 18
cache: 'pnpm'
- name: Install dependencies

View File

@@ -10,9 +10,9 @@ jobs:
lucide-font:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,9 +13,9 @@ jobs:
lucide-preact:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,9 +13,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -29,9 +29,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -14,9 +14,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -30,9 +30,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -10,9 +10,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,9 +13,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -29,9 +29,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -11,9 +11,9 @@ jobs:
lucide-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,9 +13,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -29,9 +29,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,9 +13,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -29,9 +29,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -13,9 +13,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -29,9 +29,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -12,9 +12,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -28,9 +28,9 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'

View File

@@ -14,10 +14,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: 'package.json'
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
@@ -25,9 +22,9 @@ jobs:
with:
files: icons/*.svg
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
- name: Install svgson for code preview (safer and faster than installing all deps)
run: npm install svgson@5.3.1 --force
run: npm install svgson
- name: Save PR number
run: |
@@ -35,7 +32,7 @@ jobs:
echo ${{ github.event.number }} > ./pr/NR
- name: Generate comment markup
run: node ./scripts/generateChangedIconsCommentMarkup.mts >> ./pr/comment-markup.md
run: node ./scripts/generateChangedIconsCommentMarkup.mjs >> ./pr/comment-markup.md
id: comment-markup
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}

View File

@@ -1,37 +0,0 @@
name: Pull request tags suggestions
on:
pull_request_target:
paths:
- 'icons/*.json'
jobs:
pull-request-tags-suggestions:
name: Pull Request Tags Suggestions
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# We checkout the main branch of main repository for security reasons.
# This is to prevent the workflow from running on a forked repository.
- uses: actions/checkout@v6
with:
repository: lucide-icons/lucide
- 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: Generate comment markup
run: node ./scripts/suggestTags.mts
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PULL_REQUEST_NUMBER: ${{ github.event.number }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -18,13 +18,9 @@ on:
description: Version
required: true
permissions:
id-token: write # Required for OIDC
contents: write
jobs:
pre-release:
if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere", "jguddas"]', github.actor)
if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere"]', github.actor)
runs-on: ubuntu-latest
outputs:
VERSION: ${{ steps.get_version.outputs.VERSION }}
@@ -43,8 +39,7 @@ jobs:
runs-on: ubuntu-latest
needs: pre-release
permissions:
id-token: write # Required for OIDC
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
@@ -53,6 +48,7 @@ jobs:
'lucide',
'lucide-react',
'lucide-react-native',
'lucide-vue',
'lucide-vue-next',
'lucide-angular',
'lucide-preact',
@@ -62,9 +58,9 @@ jobs:
'@lucide/svelte',
]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -72,6 +68,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set Auth Token
run: npm config set //registry.npmjs.org/:_authToken ${{ inputs.NPM_TOKEN || secrets.NPM_TOKEN }}
- name: Set new version
run: pnpm --filter ${{ matrix.package }} version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version
@@ -91,14 +90,12 @@ jobs:
runs-on: ubuntu-latest
needs: [pre-release, lucide-font]
permissions:
id-token: write # Required for OIDC
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -106,6 +103,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set Auth Token
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
- name: Set new version
run: pnpm --filter lucide-static version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version
@@ -125,9 +125,9 @@ jobs:
runs-on: ubuntu-latest
needs: pre-release
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
node-version-file: 'package.json'
@@ -151,11 +151,9 @@ jobs:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
needs: [pre-release, lucide-font]
permissions:
id-token: write # Required for OIDC
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- name: Zip font and icons
run: |

View File

@@ -2,8 +2,7 @@ name: 'Request Review'
on:
pull_request_target:
types: [opened]
paths:
- icons/*.svg
paths: icons/*.svg
jobs:
request-review:
@@ -13,7 +12,7 @@ jobs:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge

284
.github/workflows/version-up.sh vendored Executable file
View File

@@ -0,0 +1,284 @@
#!/usr/bin/env bash
## Copyright (C) 2017, Oleksandr Kucherenko
## Last revisit: 2017-09-29
## get highest version tag for all branches
function highest_tag(){
local TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
echo "$TAG"
}
## extract current branch name
function current_branch(){
## expected: heads/{branch_name}
## expected: {branch_name}
local BRANCH=$(git rev-parse --abbrev-ref HEAD | cut -d"/" -f2)
echo "$BRANCH"
}
## get latest/head commit hash number
function head_hash(){
local COMMIT_HASH=$(git rev-parse --verify HEAD)
echo "$COMMIT_HASH"
}
## extract tag commit hash code, tag name provided by argument
function tag_hash(){
local TAG_HASH=$(git log -1 --format=format:"%H" $1 2>/dev/null | tail -n1)
echo "$TAG_HASH"
}
## get latest revision number
function latest_revision(){
local REV=$(git rev-list --count HEAD 2>/dev/null)
echo "$REV"
}
## parse last found tag, extract it PARTS
function parse_last(){
local position=$(($1-1))
# two parts found only
local SUBS=( ${PARTS[$position]//-/ } )
#echo ${SUBS[@]}, size: ${#SUBS}
# found NUMBER
PARTS[$position]=${SUBS[0]}
#echo ${PARTS[@]}
# found SUFFIX
if [[ ${#SUBS} -ge 1 ]]; then
PARTS[4]=${SUBS[1],,} #lowercase
#echo ${PARTS[@]}, ${SUBS[@]}
fi
}
## increment REVISION part, don't touch STAGE
function increment_revision(){
PARTS[3]=$(( PARTS[3] + 1 ))
IS_DIRTY=1
}
## increment PATCH part, reset all other lower PARTS, don't touch STAGE
function increment_patch(){
PARTS[2]=$(( PARTS[2] + 1 ))
PARTS[3]=0
IS_DIRTY=1
}
## increment MINOR part, reset all other lower PARTS, don't touch STAGE
function increment_minor(){
PARTS[1]=$(( PARTS[1] + 1 ))
PARTS[2]=0
PARTS[3]=0
IS_DIRTY=1
}
## increment MAJOR part, reset all other lower PARTS, don't touch STAGE
function incremet_major(){
PARTS[0]="v$(( PARTS[0] + 1 ))"
PARTS[1]=0
PARTS[2]=0
PARTS[3]=0
IS_DIRTY=1
}
## increment the number only of last found PART: REVISION --> PATCH --> MINOR. don't touch STAGE
function increment_last_found(){
if [[ "${#PARTS[3]}" == 0 || "${PARTS[3]}" == "0" ]]; then
if [[ "${#PARTS[2]}" == 0 || "${PARTS[2]}" == "0" ]]; then
increment_minor
else
increment_patch
fi
else
increment_revision
fi
# stage part is not EMPTY
if [[ "${#PARTS[4]}" != 0 ]]; then
IS_SHIFT=1
fi
}
## compose version from PARTS
function compose(){
MAJOR="${PARTS[0]}"
MINOR=".${PARTS[1]}"
PATCH=".${PARTS[2]}"
REVISION=".${PARTS[3]}"
SUFFIX="-${PARTS[4]}"
if [[ "${#PATCH}" == 1 ]]; then # if empty {PATCH}
PATCH=""
fi
if [[ "${#REVISION}" == 1 ]]; then # if empty {REVISION}
REVISION=""
fi
if [[ "${PARTS[3]}" == "0" ]]; then # if revision is ZERO
REVISION=""
fi
# shrink patch and revision
if [[ -z "${REVISION// }" ]]; then
if [[ "${PARTS[2]}" == "0" ]]; then
PATCH=".0"
fi
else # revision is not EMPTY
if [[ "${#PATCH}" == 0 ]]; then
PATCH=".0"
fi
fi
# remove suffix if we don't have a alpha/beta/rc
if [[ "${#SUFFIX}" == 1 ]]; then
SUFFIX=""
fi
echo "${MAJOR}${MINOR}${PATCH}${REVISION}${SUFFIX}" #full format
}
# initial version used for repository without tags
INIT_VERSION=0.0.0.0-alpha
# do GIT data extracting
TAG=$(highest_tag)
REVISION=$(latest_revision)
BRANCH=$(current_branch)
TAG_HASH=$(tag_hash $TAG)
HEAD_HASH=$(head_hash)
# if tag and branch commit hashes are different, than print info about that
#echo $HEAD_HASH vs $TAG_HASH
if [[ "$@" == "" ]]; then
if [[ "$TAG_HASH" == "$HEAD_HASH" ]]; then
echo "Tag $TAG and HEAD are aligned. We will stay on the TAG version."
echo ""
NO_ARGS_VALUE='--stay'
else
PATTERN="^[0-9]+.[0-9]+(.[0-9]+)*(-(alpha|beta|rc))*$"
if [[ "$BRANCH" =~ $PATTERN ]]; then
echo "Detected version branch '$BRANCH'. We will auto-increment the last version PART."
echo ""
NO_ARGS_VALUE='--default'
else
echo "Detected branch name '$BRANCH' than does not match version pattern. We will increase MINOR."
echo ""
NO_ARGS_VALUE='--minor'
fi
fi
fi
#
# {MAJOR}.{MINOR}[.{PATCH}[.{REVISION}][-(.*)]
#
# Suffix: alpha, beta, rc
# No Suffix --> {NEW_VERSION}-alpha
# alpha --> beta
# beta --> rc
# rc --> {VERSION}
#
PARTS=( ${TAG//./ } )
parse_last ${#PARTS[@]} # array size as argument
#echo ${PARTS[@]}
# if no parameters than emulate --default parameter
if [[ "$@" == "" ]]; then
set -- $NO_ARGS_VALUE
fi
# parse input parameters
for i in "$@"
do
key="$i"
case $key in
-a|--alpha) # switched to ALPHA
PARTS[4]="alpha"
IS_SHIFT=1
;;
-b|--beta) # switched to BETA
PARTS[4]="beta"
IS_SHIFT=1
;;
-c|--release-candidate) # switched to RC
PARTS[4]="rc"
IS_SHIFT=1
;;
-r|--release) # switched to RELEASE
PARTS[4]=""
IS_SHIFT=1
;;
-p|--patch) # increment of PATCH
increment_patch
;;
-e|--revision) # increment of REVISION
increment_revision
;;
-g|--git-revision) # use git revision number as a revision part§
PARTS[3]=$(( REVISION ))
IS_DIRTY=1
;;
-i|--minor) # increment of MINOR by default
increment_minor
;;
--default) # stay on the same stage, but increment only last found PART of version code
increment_last_found
;;
-m|--major) # increment of MAJOR
incremet_major
;;
-s|--stay) # extract version info
IS_DIRTY=1
NO_APPLY_MSG=1
;;
-t|--tag-only) # extract version info
TAG_ONLY=1
;;
--apply)
DO_APPLY=1
;;
-h|--help)
help
;;
esac
shift
done
# detected shift, but no increment
if [[ "$IS_SHIFT" == "1" ]]; then
# temporary disable stage shift
stage=${PARTS[4]}
PARTS[4]=''
# detect first run on repository, INIT_VERSION was used
if [[ "$(compose)" == "0.0" ]]; then
increment_minor
fi
PARTS[4]=$stage
fi
# no increment applied yet and no shift of state, do minor increase
if [[ "$IS_DIRTY$IS_SHIFT" == "" ]]; then
increment_minor
fi
compose
# is proposed tag in conflict with any other TAG
PROPOSED_HASH=$(tag_hash $(compose))
if [[ "${#PROPOSED_HASH}" -gt 0 && "$NO_APPLY_MSG" == "" ]]; then
echo -e "\033[31mERROR:\033[0m "
echo -e "\033[31mERROR:\033[0m Found conflict with existing tag \033[32m$(compose)\033[0m / $PROPOSED_HASH"
echo -e "\033[31mERROR:\033[0m Only manual resolving is possible now."
echo -e "\033[31mERROR:\033[0m "
echo -e "\033[31mERROR:\033[0m To Resolve try to add --revision or --patch modifier."
echo -e "\033[31mERROR:\033[0m "
echo ""
exit 1
fi

3
.gitignore vendored
View File

@@ -4,7 +4,6 @@
.obsidian
.now
.idea
.env
node_modules
dist
build
@@ -44,7 +43,7 @@ docs/.vitepress/data/releaseMetaData
docs/.vitepress/data/categoriesData.json
docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vitepress/data/brandStopwords.json
docs/.vercel
docs/.nitro
.gitignore

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
auto-install-peers=true

View File

@@ -1,5 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/Yash-Singh1/vscode-snippets-json-schema/main/schema.json",
"Lucide SVG": {
"scope": "xml",
"description": "Base SVG with Lucide attributes.",
@@ -52,16 +51,6 @@
],
"body": "<circle cx=\"${2:12}\" cy=\"${3:$2}\" r=\"${1|10,2,.5\" fill=\"currentColor|}\" />"
},
"Squircle": {
"scope": "xml",
"description": "SVG `path` with Lucide defaults.",
"prefix": [
"squircle",
"path",
"<path"
],
"body": "<path d=\"M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9-9-1.8-9-9 1.8-9 9-9\" />"
},
"Ellipse": {
"scope": "xml",
"description": "SVG `ellipse`.",

View File

@@ -1,71 +0,0 @@
# Our Official Stance on Including Brand Logos in Lucide
## TL;DR
Lucide **does not accept** brand logos, and we do not plan to add them in the future.
This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
If you need brand logos, we recommend [Simple Icons](https://simpleicons.org/), which provides an extensive, legally safer collection of brand marks.
---
## 1. Historical Context
This is not a new debate — other icon sets have gone through the same discussion:
- **Material Design Icons** [deprecated all brand icons](https://github.com/Templarian/MaterialDesign/issues/6602) because they didn't fit the style, didn't work well in one color, and often looked out of place in a 24×24px grid.
- **Feather Icons** [came to the same conclusion](https://github.com/feathericons/feather/issues/763): brand logos have their own style, and forcing them into another inevitably leads to aesthetic compromises.
- **Lucide** learned from these examples — we'd rather focus on making a consistent set of non-brand icons that all work together.
## 2. Legal Considerations
Most brand logos:
- Are **protected by trademark or copyright**.
- Have **strict rules** for how they can be used (colors, spacing, proportions, etc.).
- **Don't allow modification** — but we'd have to change them to fit Lucide's style.
This means adding them could:
1. Break copyright or trademark law.
2. Make both you and the Lucide project legally responsible.
3. Force us to review every new request one by one for legal issues — something we simply can't do.
> **Note:** Simple Icons avoids this by keeping logos exactly as the brand provides them — though even they sometimes face [legal challenges](https://github.com/simple-icons/simple-icons/issues/11236).
## 3. Design & Consistency
Lucide is all about **visual consistency**.
Adding brand logos would:
- Break [our own design rules](https://lucide.dev/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".
If the logos are not in Lucide's style, why include them in Lucide at all? Better to use them from a dedicated brand icon source.
## 4. Maintenance Burden
Even with our current **"no brand icon requests"** policy, people still request them regularly.
Having any brand icons in the set:
- Makes people think we might add more in the future.
- Leads to repeated requests and the same conversations over and over.
- Wastes maintainer time redirecting people to the same explanation.
Removing them entirely solves this problem.
## 5. Recommended Alternatives
If you need brand icons, try:
- [Simple Icons](https://simpleicons.org/): offers a huge range of brands, in consistent SVG format, using a 24×24 viewBox, the same as ours.
- Official brand asset pages: most major companies provide downloadable SVGs.
You can use these alongside Lucide without bloating our core library.
## Final Words
Lucide is an **icon** set, not a **logo** set.
Logos belong in dedicated logo resources.
We're focusing on what Lucide does best: providing a clean, cohesive, and legally safe collection of open-source icons.

View File

@@ -161,30 +161,6 @@ lucide
The lucide.dev website is using [vitepress](https://vitepress.dev/) to generate the static website. The markdown files are located in the docs directory.
#### Running the Docs Website Locally
To test the docs website locally, follow these steps:
1. **Navigate to the docs directory**
```sh
cd docs
```
2. **Start the Local Development Server**
```sh
pnpm run docs:dev
```
3. **Open the Website Locally**
Vitepress should open with the following format:
VitePress dev server is running at:
- **Local**: `http://localhost:3000/`
- **Network**: `http://192.168.x.x:3000/`
### Guides
Detailed documentation about: installation, guides, packages, design guides etc.
@@ -197,6 +173,8 @@ All the icons of lucide in SVG format. These will be used as source for all the
Includes all the (npm) packages of lucide.
> Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and uses pub for publishing.
### Scripts
Includes useful scripts to automate certain jobs. Big part of the scripts is the template generation, for example it generates icon components for all the packages. These scripts are usually executed from the "scripts" section in the package.json.

26
LICENSE
View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2023 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2025.
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@@ -13,27 +13,3 @@ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
The MIT License (MIT) (for portions derived from Feather)
Copyright (c) 2013-2023 Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -57,12 +57,6 @@ For more info on how to contribute please see the [contribution guidelines](http
Caught a mistake or want to contribute to the documentation? [Edit this page on Github](https://github.com/lucide-icons/lucide/blob/main/README.md)
## About brand logos
Lucide **does not accept** brand logos, and we do not plan to add them in the future. This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
[Click here to read our official statement about brand logos in Lucide.](./BRAND_LOGOS_STATEMENT.md)
## Community
Join the community on our [Discord](https://discord.gg/EH6nSts) server!
@@ -90,5 +84,5 @@ Thank you to all the people who contributed to Lucide!
### Awesome backers 🍺
<a href="https://www.scipress.io?utm_source=lucide"><img src="docs/public/sponsors/scipress.svg" width="180" alt="Scipress sponsor badge" /></a>
<a href="https://github.com/pdfme/pdfme"><img src="docs/public/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a>
<a href="https://www.fina.money/"><img src="docs/public/sponsors/fina-money.png" width="180" alt="Fina Money sponsor badge" /></a>

View File

@@ -1,149 +0,0 @@
{
"adobe": "Adobe",
"airplay": "AirPlay",
"amazon": "Amazon",
"angular": "Angular",
"aws": "AWS",
"azure": "Azure",
"bandcamp": "Bandcamp",
"behance": "Behance",
"bitbucket": "Bitbucket",
"blender": "Blender",
"bluesky": "BlueSky",
"bootstrap": "Bootstrap",
"brave": "Brave",
"chakra": "Chakra UI",
"chrome": "Chrome",
"codepen": "Codepen",
"codesandbox": "CodeSandbox",
"csharp": "C#",
"cypress": "Cypress",
"dart": "Dart",
"deezer": "Deezer",
"deno": "Deno",
"discord": "Discord",
"docker": "Docker",
"dribbble": "Dribbble",
"dropbox": "Dropbox",
"edge": "Edge",
"ember": "Ember",
"epic": "Epic Games",
"erlang": "Erlang",
"esbuild": "esbuild",
"eslint": "ESLint",
"facebook": "Facebook",
"figjam": "FigJam",
"figma": "Figma",
"firebase": "Firebase",
"firefox": "Firefox",
"framer": "Framer",
"gatsby": "Gatsby",
"gcp": "Google Cloud",
"github": "GitHub",
"gitlab": "GitLab",
"golang": "GoLang",
"google": "Google",
"gmail": "Gmail",
"gravatar": "Gravatar",
"haskell": "Haskell",
"instagram": "Instagram",
"java": "Java",
"javascript": "JavaScript",
"jest": "Jest",
"jira": "Jira",
"kotlin": "Kotlin",
"kubernetes": "Kubernetes",
"less": "Less",
"leetcode": "LeetCode",
"leet-code": "LeetCode",
"line": "LINE",
"linkedin": "LinkedIn",
"lua": "Lua",
"mariadb": "MariaDB",
"mcp": "MCP",
"messenger": "Messenger",
"microsoft": "Microsoft",
"mongodb": "MongoDB",
"mui": "Material UI",
"mysql": "MySQL",
"nestjs": "NestJS",
"netflix": "Netflix",
"netlify": "Netlify",
"next": "Next.js",
"nodejs": "Node.js",
"notion": "Notion",
"nostr": "Nostr",
"npm": "npm",
"nuxt": "Nuxt",
"opera": "Opera",
"oracle": "Oracle",
"patreon": "Patreon",
"paypal": "PayPal",
"perl": "Perl",
"php": "PHP",
"pinterest": "Pinterest",
"pix": "PiX",
"playstation": "PlayStation",
"playwright": "Playwright",
"pnpm": "pnpm",
"postcss": "PostCSS",
"postgresql": "PostgreSQL",
"prettier": "Prettier",
"prisma": "Prisma",
"python": "Python",
"qwik": "Qwik",
"react": "React",
"reddit": "Reddit",
"redis": "Redis",
"rollup": "Rollup",
"rust": "Rust",
"safari": "Safari",
"sass": "Sass",
"scala": "Scala",
"scss": "Sass",
"semantic": "Semantic UI",
"shopify": "Shopify",
"skype": "Skype",
"slack": "Slack",
"solid": "SolidJS",
"soundcloud": "SoundCloud",
"spotify": "Spotify",
"sqlite": "SQLite",
"squarespace": "Squarespace",
"steam": "Steam",
"stripe": "Stripe",
"substack": "Substack",
"supabase": "Supabase",
"surge": "Surge",
"svelte": "Svelte",
"swift": "Swift",
"tailwind": "Tailwind CSS",
"telegram": "Telegram",
"terraform": "Terraform",
"tesla": "Tesla",
"tidal": "Tidal",
"tiktok": "TikTok",
"trello": "Trello",
"twitch": "Twitch",
"twitter": "Twitter",
"typescript": "TypeScript",
"unity": "Unity",
"unreal": "Unreal Engine",
"vercel": "Vercel",
"vimeo": "Vimeo",
"vite": "Vite",
"vitest": "Vitest",
"vue": "Vue",
"webpack": "Webpack",
"wechat": "WeChat",
"whatsapp": "WhatsApp",
"windows": "Windows",
"wix": "Wix",
"x.com": "X.com",
"x-social": "X.com",
"xbox": "Xbox",
"yarn": "Yarn",
"youtube": "YouTube",
"zig": "Zig",
"zoom": "Zoom"
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Navigation, Maps, and POIs",
"title": "Navigation",
"icon": "compass"
}

View File

@@ -13,12 +13,10 @@ export default eventHandler((event) => {
const data = pathData.at(-1).slice(0, -4);
const [name] = pathData;
const src = Buffer.from(data, 'base64').toString('utf8').replaceAll('\n', '');
const width = parseInt((src.includes('<svg ') ? src.match(/width="(\d+)"/)?.[1] : null) ?? '24');
const height = parseInt(
(src.includes('<svg ') ? src.match(/height="(\d+)"/)?.[1] : null) ?? '24',
);
const src = Buffer.from(data, 'base64')
.toString('utf8')
.replaceAll('\n', '')
.replace(/<svg[^>]*>|<\/svg>/g, '');
const children = [];
@@ -40,7 +38,7 @@ export default eventHandler((event) => {
children.push(
createElement(Backdrop, {
backdropString,
src: src.replace(/<svg[^>]*>|<\/svg>/g, ''),
src,
color: '#777',
}),
);
@@ -48,18 +46,7 @@ export default eventHandler((event) => {
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(
createElement(
SvgPreview,
{
src: src.replace(/<svg[^>]*>|<\/svg>/g, ''),
height,
width,
showGrid: true,
},
children,
),
),
renderToString(createElement(SvgPreview, { src, showGrid: true }, children)),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');

View File

@@ -17,13 +17,6 @@ export default eventHandler((event) => {
.replaceAll('\n', '')
.replace(/<svg[^>]*>|<\/svg>/g, '');
const width = parseInt(
(newSrc.includes('<svg ') ? newSrc.match(/width="(\d+)"/)?.[1] : null) ?? '24',
);
const height = parseInt(
(newSrc.includes('<svg ') ? newSrc.match(/height="(\d+)"/)?.[1] : null) ?? '24',
);
const children = [];
const oldSrc = iconNodes[name]
@@ -34,9 +27,7 @@ export default eventHandler((event) => {
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(
createElement(Diff, { oldSrc, newSrc, showGrid: true, height, width }, children),
),
renderToString(createElement(Diff, { oldSrc, newSrc, showGrid: true }, children)),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');

View File

@@ -1,10 +1,6 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { Resvg, initWasm } from '@resvg/resvg-wasm';
import iconNodes from '../../../data/iconNodes';
import wasm from './loadWasm';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
var initializedResvg = initWasm(wasm);
@@ -13,37 +9,27 @@ export default eventHandler(async (event) => {
await initializedResvg;
const imageSize = 96;
const name = params.data.split('/').at(-3);
const iconSizeString = params.data.split('/').at(-2);
const svgData = params.data.split('/').at(-1);
const [iconSizeString, svgData] = params.data.split('/');
const iconSize = parseInt(iconSizeString, 10);
const data = svgData.slice(0, -4);
const prevSvg = iconNodes[name]
? renderToStaticMarkup(createElement(createLucideIcon(name, iconNodes[name])))
: undefined;
const src = Buffer.from(data, 'base64').toString('utf8');
const svg = (src.includes('<svg') ? src : `<svg>${src}</svg>`)
.replace(/(\r\n|\n|\r)/gm, '')
.replace(
/<svg[^>]*>/,
/<svg[^>]*/,
`<svg
xmlns="http://www.w3.org/2000/svg"
width="${iconSize}"
height="${prevSvg ? iconSize * 2 : iconSize}"
viewBox="0 0 24 ${prevSvg ? 48 : 24}"
height="${iconSize}"
viewBox="0 0 24 24"
fill="none"
stroke="#fff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
${prevSvg?.replaceAll('\n', '').replace(/<svg[^>]*>|<\/svg>/g, '')}
<g transform="translate(0, ${prevSvg ? 24 : 0})">
`,
)
.replace(/<\/svg>/, '</g></svg>');
);
const resvg = new Resvg(svg, { background: '#000' });
const pngData = resvg.render();
@@ -53,7 +39,7 @@ export default eventHandler(async (event) => {
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return `
<svg xmlns="http://www.w3.org/2000/svg" width="${imageSize}" height="${prevSvg ? imageSize * 2 : imageSize}" viewBox="0 0 ${imageSize} ${prevSvg ? imageSize * 2 : imageSize}">
<svg xmlns="http://www.w3.org/2000/svg" width="${imageSize}" height="${imageSize}" viewBox="0 0 ${imageSize} ${imageSize}">
<style>
@media screen and (prefers-color-scheme: light) {
#fallback-background { fill: transparent; }
@@ -66,20 +52,20 @@ export default eventHandler(async (event) => {
<mask id="mask">
<image
width="${imageSize}"
height="${prevSvg ? imageSize * 2 : imageSize}"
height="${imageSize}"
href="data:image/png;base64,${pngBuffer.toString('base64')}"
image-rendering="pixelated"
/>
</mask>
<rect
id="fallback-background"
width="100%"
height="100%" ry="${imageSize / 24}"
width="${imageSize}"
height="${imageSize}" ry="${imageSize / 24}"
fill="#fff"
/>
<rect
width="100%"
height="100%"
width="${imageSize}"
height="${imageSize}"
fill="#000"
mask="url(#mask)"
/>

View File

@@ -9,7 +9,6 @@ if (process.env.NODE_ENV === 'development') {
wasm = fs.readFileSync(require.resolve('@resvg/resvg-wasm/index_bg.wasm'));
} else {
// @ts-ignore
wasm = resvg_wasm;
}

View File

@@ -1,55 +0,0 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString } from 'react-dom/server';
import { createElement } from 'react';
import Diff from '../../../lib/SvgPreview/Diff.tsx';
export default eventHandler((event) => {
const { params } = event.context;
const pathData = params.data.split('/');
const data = pathData.at(-1).slice(0, -4);
const [operation] = pathData;
const newSrc = Buffer.from(data, 'base64')
.toString('utf8')
.replaceAll('\n', '')
.replace(/<svg[^>]*>|<\/svg>/g, '');
const width = parseInt(
(newSrc.includes('<svg ') ? newSrc.match(/width="(\d+)"/)?.[1] : null) ?? '24',
);
const height = parseInt(
(newSrc.includes('<svg ') ? newSrc.match(/height="(\d+)"/)?.[1] : null) ?? '24',
);
const children = [];
let oldSrc = '';
if (operation.startsWith('rotate-')) {
const degrees = parseInt(operation.replace('rotate-', ''));
if (isNaN(degrees)) return '';
oldSrc = `<g transform="rotate(${degrees} ${width / 2} ${height / 2})">${newSrc}</g>`;
} else if (operation === 'flip-horizontal') {
oldSrc = `<g transform="scale(1, -1) translate(0, -${height})">${newSrc}</g>`;
} else if (operation === 'flip-vertical') {
oldSrc = `<g transform="scale(-1, 1) translate(-${width}, 0)">${newSrc}</g>`;
} else if (operation === 'flip-backslash') {
oldSrc = `<g transform="rotate(90, ${width / 2}, ${height / 2}) scale(1, -1) translate(0, -${height})">${newSrc}</g>`;
} else if (operation === 'flip-slash') {
oldSrc = `<g transform="rotate(90, ${width / 2}, ${height / 2}) scale(-1, 1) translate(-${width}, 0)">${newSrc}</g>`;
} else {
return '';
}
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(
createElement(Diff, { oldSrc, newSrc, showGrid: true, height, width }, children),
),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return svg;
});

View File

@@ -36,13 +36,6 @@ export default defineConfig({
},
},
head: [
[
'link',
{
rel: 'preconnect',
href: 'https://analytics.lucide.dev',
},
],
[
'script',
{

View File

@@ -101,7 +101,7 @@
},
{
"name": "navigation",
"title": "Navigation, Maps, and POIs"
"title": "Navigation"
},
{
"name": "notifications",

View File

@@ -7,14 +7,6 @@
"dark": "/company-logos/vercel-dark.svg"
}
},
{
"name": "MDN Web Docs",
"url": "https://developer.mozilla.org/",
"image": {
"light": "/company-logos/mdn-light.svg",
"dark": "/company-logos/mdn-dark.svg"
}
},
{
"name": "Supabase",
"url": "https://supabase.com",
@@ -31,14 +23,6 @@
"dark": "/company-logos/obsidian-dark.svg"
}
},
{
"name": "Nuxt",
"url": "https://nuxt.com/",
"image": {
"light": "/company-logos/nuxt-light.svg",
"dark": "/company-logos/nuxt-dark.svg"
}
},
{
"name": "Open Collective",
"url": "https://opencollective.com",

View File

@@ -77,8 +77,8 @@
"source": "https://github.com/swisnl/nuxt-lucide-icons",
"documentation": "https://github.com/swisnl/nuxt-lucide-icons/blob/main/README.md"
},
{
"name": "lucide_lustre",
{
"name": "lucide-lustre",
"description": "A library providing https://lucide.dev icons to lustre.",
"icon": "/framework-logos/lustre.webp",
"shields": [
@@ -95,81 +95,5 @@
],
"source": "https://github.com/dinkelspiel/lucide_lustre",
"documentation": "https://github.com/dinkelspiel/lucide_lustre/blob/master/README.md"
},
{
"name": "lucide_icons_flutter",
"description": "A library providing https://lucide.dev icons to Flutter.",
"icon": "/framework-logos/flutter.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/pub/v/lucide_icons_flutter",
"href": "https://pub.dev/packages/lucide_icons_flutter"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/pub/dm/lucide_icons_flutter",
"href": "https://pub.dev/packages/lucide_icons_flutter"
}
],
"source": "https://github.com/vqh2602/lucide-flutter-main",
"documentation": "https://pub.dev/documentation/lucide_icons_flutter/latest/"
},
{
"name": "lucide-slint",
"description": "Implementation of the lucide icon library for Slint.",
"icon": "/framework-logos/slint.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/crates/v/lucide-slint",
"href": "https://crates.io/crates/lucide-slint"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/crates/d/lucide-slint",
"href": "https://crates.io/crates/lucide-slint"
}
],
"source": "https://github.com/cnlancehu/lucide-slint",
"documentation": "https://github.com/cnlancehu/lucide-slint/blob/main/README.md"
},
{
"name": "lucide-go",
"description": "Implementation of Lucide icons for Go's html/template package.",
"icon": "/framework-logos/go.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/github/v/release/kaugesaar/lucide-go",
"href": "https://github.com/kaugesaar/lucide-go/releases"
},
{
"alt": "Go Reference",
"src": "https://pkg.go.dev/badge/github.com/kaugesaar/lucide-go.svg",
"href": "https://pkg.go.dev/github.com/kaugesaar/lucide-go"
}
],
"source": "https://github.com/kaugesaar/lucide-go",
"documentation": "https://github.com/kaugesaar/lucide-go/blob/master/README.md"
},
{
"name": "lucide-rails",
"description": "Ruby on Rails views helper method for rendering Lucide icons.",
"icon": "/framework-logos/rails.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/gem/v/lucide-rails",
"href": "https://rubygems.org/gems/lucide-rails"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/gem/dt/lucide-rails",
"href": "https://rubygems.org/gems/lucide-rails"
}
],
"source": "https://github.com/heyvito/lucide-rails",
"documentation": "https://github.com/heyvito/lucide-rails/blob/master/README.md"
}
]

View File

@@ -53,8 +53,8 @@ const Backdrop = ({
<rect
x="0"
y="0"
width="100%"
height="100%"
width="24"
height="24"
fill="#fff"
stroke="none"
/>
@@ -67,8 +67,8 @@ const Backdrop = ({
<rect
x="0"
y="0"
width="100%"
height="100%"
width="24"
height="24"
opacity={0.5}
fill={`url(#pattern-${id})`}
stroke="none"

View File

@@ -7,17 +7,15 @@ const SvgPreview = React.forwardRef<
{
oldSrc: string;
newSrc: string;
height: number;
width: number;
} & React.SVGProps<SVGSVGElement>
>(({ oldSrc, newSrc, children, height, width, ...props }, ref) => {
>(({ oldSrc, newSrc, children, ...props }, ref) => {
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
@@ -27,8 +25,6 @@ const SvgPreview = React.forwardRef<
>
<style>{darkModeCss}</style>
<Grid
width={width}
height={height}
strokeWidth={0.1}
stroke="#777"
strokeOpacity={0.3}
@@ -41,8 +37,8 @@ const SvgPreview = React.forwardRef<
<rect
x="0"
y="0"
width="100%"
height="100%"
width="24"
height="24"
fill="#000"
stroke="none"
/>

View File

@@ -1,137 +0,0 @@
import React from 'react';
import pathToPoints from './path-to-points';
import { Path, PathProps } from './types';
export const GapViolationHighlight = ({
radius,
stroke,
strokeWidth,
strokeOpacity,
paths,
...props
}: {
paths: Path[];
} & PathProps<'stroke' | 'strokeOpacity' | 'strokeWidth', 'd'>) => {
const id = React.useId();
const groupedPaths = Object.entries(
paths.reduce(
(groups, val) => {
const key = val.c.id;
groups[key] = [...(groups[key] || []), val];
return groups;
},
{} as Record<number, Path[]>,
),
);
const groups: Group[] = [];
for (const [, paths] of groupedPaths) {
const d = paths.map((path) => path.d).join(' ');
const points = paths.flatMap((path) => pathToPoints(path));
groups.push({ id: d, points });
}
const mergedGroups = mergeGroups(groups, 2);
return (
<g {...props}>
<defs xmlns="http://www.w3.org/2000/svg">
<pattern
id={`backdrop-pattern-${id}`}
width=".1"
height=".1"
patternUnits="userSpaceOnUse"
patternTransform="rotate(45 50 50)"
>
<line
stroke={stroke}
strokeWidth={0.1}
y2={1}
/>
<line
stroke={stroke}
strokeWidth={0.1}
y2={1}
/>
</pattern>
</defs>
{mergedGroups.flatMap((ds, idx, arr) =>
arr.slice(0, idx).map((val, i) => (
<g
strokeWidth={strokeWidth}
key={i}
>
<mask
id={`svg-preview-backdrop-mask-${id}-${i}`}
maskUnits="userSpaceOnUse"
>
<path
stroke="white"
d={val.join(' ')}
/>
</mask>
<path
d={ds.join(' ')}
stroke={`url(#backdrop-pattern-${id})`}
strokeWidth={strokeWidth}
strokeOpacity={strokeOpacity}
mask={`url(#svg-preview-backdrop-mask-${id}-${i})`}
/>
</g>
)),
)}
</g>
);
};
type Point = { x: number; y: number };
type Group = { id: string; points: Point[] };
// Euclidean distance
function distance(a: Point, b: Point): number {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// Check if two groups should be merged based on minimum distance
function shouldMerge(a: Group, b: Group, minDistance: number): boolean {
for (const pa of a.points) {
for (const pb of b.points) {
if (distance(pa, pb) <= minDistance) {
return true;
}
}
}
return false;
}
// Merge groups and return arrays of merged group IDs
function mergeGroups(groups: Group[], minDistance: number): string[][] {
const mergedGroups: Group[][] = groups.map((g) => [g]);
let changed = true;
while (changed) {
changed = false;
outer: for (let i = 0; i < mergedGroups.length; i++) {
for (let j = i + 1; j < mergedGroups.length; j++) {
// Check if any group in mergedGroups[i] should merge with any in mergedGroups[j]
if (
mergedGroups[i].some((ga) =>
mergedGroups[j].some((gb) => shouldMerge(ga, gb, minDistance)),
)
) {
// Merge group j into group i
mergedGroups[i] = [...mergedGroups[i], ...mergedGroups[j]];
mergedGroups.splice(j, 1);
changed = true;
break outer;
}
}
}
}
// Return only arrays of IDs
return mergedGroups.map((groupList) => groupList.map((g) => g.id));
}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { PathProps, Path } from './types';
import getPaths, { assert } from './utils';
import { GapViolationHighlight } from './GapViolationHighlight.tsx';
import { getPaths, assert } from './utils';
export const darkModeCss = `
@media screen and (prefers-color-scheme: light) {
@@ -21,16 +20,10 @@ export const darkModeCss = `
export const Grid = ({
radius,
fill,
height,
width,
subGridSize = 0,
fill = '#fff',
...props
}: {
height: number;
width: number;
strokeWidth: number;
subGridSize?: number;
radius: number;
} & PathProps<'stroke', 'strokeWidth'>) => (
<g
@@ -40,53 +33,43 @@ export const Grid = ({
>
<rect
className="svg-preview-grid-rect"
width={width - props.strokeWidth}
height={height - props.strokeWidth}
width={24 - props.strokeWidth}
height={24 - props.strokeWidth}
x={props.strokeWidth / 2}
y={props.strokeWidth / 2}
rx={radius}
fill={fill}
/>
<path
strokeDasharray={
'0 0.1 ' + '0.1 0.15 '.repeat(subGridSize ? subGridSize * 4 - 1 : 95) + '0 0.15'
}
strokeDasharray={'0 0.1 ' + '0.1 0.15 '.repeat(11) + '0 0.15'}
strokeWidth={0.1}
d={
props.d ||
[
...new Array(Math.floor(width - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => !subGridSize || i % subGridSize !== subGridSize - 1)
.flatMap((i) => [`M${i + 1} ${props.strokeWidth}v${height - props.strokeWidth * 2}`]),
...new Array(Math.floor(height - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => !subGridSize || i % subGridSize !== subGridSize - 1)
.flatMap((i) => [`M${props.strokeWidth} ${i + 1}h${width - props.strokeWidth * 2}`]),
].join('')
new Array(Math.floor(24 - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => i % 3 !== 2)
.flatMap((i) => [
`M${props.strokeWidth} ${i + 1}h${24 - props.strokeWidth * 2}`,
`M${i + 1} ${props.strokeWidth}v${24 - props.strokeWidth * 2}`,
])
.join('')
}
/>
<path
d={
props.d ||
new Array(Math.floor(24 - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => i % 3 === 2)
.flatMap((i) => [
`M${props.strokeWidth} ${i + 1}h${24 - props.strokeWidth * 2}`,
`M${i + 1} ${props.strokeWidth}v${24 - props.strokeWidth * 2}`,
])
.join('')
}
/>
{!!subGridSize && (
<path
d={
props.d ||
[
...new Array(Math.floor(width - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => i % subGridSize === subGridSize - 1)
.flatMap((i) => [`M${i + 1} ${props.strokeWidth}v${height - props.strokeWidth * 2}`]),
...new Array(Math.floor(height - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => i % subGridSize === subGridSize - 1)
.flatMap((i) => [`M${props.strokeWidth} ${i + 1}h${width - props.strokeWidth * 2}`]),
].join('')
}
/>
)}
</g>
);
@@ -116,7 +99,6 @@ const Shadow = ({
>
{groupedPaths.map(([id, paths]) => (
<mask
key={`svg-preview-shadow-mask-${id}`}
id={`svg-preview-shadow-mask-${id}`}
maskUnits="userSpaceOnUse"
strokeOpacity="1"
@@ -126,8 +108,8 @@ const Shadow = ({
<rect
x={0}
y={0}
width="100%"
height="100%"
width={24}
height={24}
fill="#fff"
stroke="none"
rx={radius}
@@ -170,34 +152,30 @@ const ColoredPath = ({
colors,
paths,
...props
}: { paths: Path[]; colors: string[] } & PathProps<never, 'd' | 'stroke'>) => {
let idx = 0;
return (
<g
className="svg-preview-colored-path-group"
{...props}
>
{paths.map(({ d, c }, i) => (
<path
key={i}
d={d}
stroke={colors[(c.name === 'path' ? idx++ : c.id) % colors.length]}
/>
))}
</g>
);
};
}: { paths: Path[]; colors: string[] } & PathProps<never, 'd' | 'stroke'>) => (
<g
className="svg-preview-colored-path-group"
{...props}
>
{paths.map(({ d, c }, i) => (
<path
key={i}
d={d}
stroke={colors[(c.name === 'path' ? i : c.id) % colors.length]}
/>
))}
</g>
);
const ControlPath = ({
paths,
radius,
pointSize,
...props
}: {
pointSize: number;
paths: Path[];
radius: number;
} & PathProps<'stroke' | 'strokeWidth', 'd'>) => {
}: { pointSize: number; paths: Path[]; radius: number } & PathProps<
'stroke' | 'strokeWidth',
'd'
>) => {
const controlPaths = paths.map((path, i) => {
const element = paths.filter((p) => p.c.id === path.c.id);
const lastElement = element.at(-1)?.next;
@@ -229,8 +207,8 @@ const ControlPath = ({
<rect
x="0"
y="0"
width="100%"
height="100%"
width="24"
height="24"
fill="#fff"
stroke="none"
rx={radius}
@@ -265,7 +243,7 @@ const ControlPath = ({
)
.join('')}
/>
{controlPaths.map(({ prev, next, startMarker, endMarker }, i) => (
{controlPaths.map(({ d, prev, next, startMarker, endMarker }, i) => (
<React.Fragment key={i}>
{startMarker && (
<circle
@@ -301,37 +279,11 @@ const Radii = ({
{...props}
>
{paths.map(
({ circle, next, prev, c }, i) =>
({ c, prev, next, circle }, i) =>
circle && (
<React.Fragment key={i}>
{circle.tangentIntersection && c.name === 'path' && (
<>
<circle
cx={next.x * 2 - circle.tangentIntersection.x}
cy={next.y * 2 - circle.tangentIntersection.y}
r={0.25}
/>
<circle
cx={prev.x * 2 - circle.tangentIntersection.x}
cy={prev.y * 2 - circle.tangentIntersection.y}
r={0.25}
/>
<path
d={`M${next.x * 2 - circle.tangentIntersection.x} ${
next.y * 2 - circle.tangentIntersection.y
}L${circle.tangentIntersection.x} ${circle.tangentIntersection.y}L${prev.x * 2 - circle.tangentIntersection.x} ${
prev.y * 2 - circle.tangentIntersection.y
}`}
/>
<circle
cx={circle.tangentIntersection.x}
cy={circle.tangentIntersection.y}
r={0.25}
/>
</>
)}
{c.name === 'path' && (
<path d={`M${next.x} ${next.y}L${circle.x} ${circle.y}L${prev.x} ${prev.y}`} />
{c.name !== 'circle' && (
<path d={`M${prev.x} ${prev.y} ${circle.x} ${circle.y} ${next.x} ${next.y}`} />
)}
<circle
cy={circle.y}
@@ -361,60 +313,55 @@ const Radii = ({
const Handles = ({
paths,
...props
}: { paths: Path[] } & PathProps<'strokeWidth' | 'stroke' | 'strokeOpacity', any>) => (
<g
className="svg-preview-handles-group"
{...props}
>
{paths.map(({ c, prev, next, cp1, cp2 }, i) => (
<React.Fragment key={i}>
{cp1 && <path d={`M${prev.x} ${prev.y} ${cp1.x} ${cp1.y}`} />}
{cp1 && (
<circle
cy={cp1.y}
cx={cp1.x}
r={0.25}
/>
)}
{cp2 && <path d={`M${next.x} ${next.y} ${cp2.x} ${cp2.y}`} />}
{cp2 && (
<circle
cy={cp2.y}
cx={cp2.x}
r={0.25}
/>
)}
</React.Fragment>
))}
</g>
);
}: { paths: Path[] } & PathProps<
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
any
>) => {
return (
<g
className="svg-preview-handles-group"
{...props}
>
{paths.map(({ c, prev, next, cp1, cp2 }) => (
<>
{cp1 && <path d={`M${prev.x} ${prev.y} ${cp1.x} ${cp1.y}`} />}
{cp1 && (
<circle
cy={cp1.y}
cx={cp1.x}
r={0.25}
/>
)}
{cp2 && <path d={`M${next.x} ${next.y} ${cp2.x} ${cp2.y}`} />}
{cp2 && (
<circle
cy={cp2.y}
cx={cp2.x}
r={0.25}
/>
)}
</>
))}
</g>
);
};
const SvgPreview = React.forwardRef<
SVGSVGElement,
{
height?: number;
width?: number;
src: string | ReturnType<typeof getPaths>;
showGrid?: boolean;
} & React.SVGProps<SVGSVGElement>
>(({ src, children, height = 24, width = 24, showGrid = false, ...props }, ref) => {
const subGridSize =
Math.max(height, width) % 3 === 0
? Math.max(height, width) > 24
? 12
: 3
: Math.max(height, width) % 5 === 0
? 5
: 0;
>(({ src, children, showGrid = false, ...props }, ref) => {
const paths = typeof src === 'string' ? getPaths(src) : src;
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
@@ -425,12 +372,8 @@ const SvgPreview = React.forwardRef<
<style>{darkModeCss}</style>
{showGrid && (
<Grid
height={height}
width={width}
subGridSize={subGridSize}
strokeWidth={0.1}
stroke="#777"
mask="url(#svg-preview-bounding-box-mask)"
strokeOpacity={0.3}
radius={1}
/>
@@ -442,12 +385,6 @@ const SvgPreview = React.forwardRef<
radius={1}
strokeOpacity={0.15}
/>
<GapViolationHighlight
paths={paths}
stroke="red"
strokeOpacity={0.75}
strokeWidth={4}
/>
<Handles
paths={paths}
strokeWidth={0.12}
@@ -496,6 +433,4 @@ const SvgPreview = React.forwardRef<
);
});
SvgPreview.displayName = 'SvgPreview';
export default SvgPreview;

View File

@@ -1,19 +0,0 @@
import memoize from 'lodash/memoize';
import SVGPathCommander from 'svg-path-commander';
import { Path } from './types';
function pathToPoints({ d, prev, next }: Path, interval = 1) {
const commander = new SVGPathCommander(d);
const points = [];
try {
const totalLength = commander.getTotalLength();
points.push(prev);
for (let i = interval; i < totalLength - interval; i += interval) {
points.push(commander.getPointAtLength(i));
}
points.push(next);
} catch (err) {}
return points;
}
export default memoize(pathToPoints);

View File

@@ -1,10 +1,7 @@
import { INode, parseSync } from 'svgson';
// @ts-ignore
import toPath from 'element-to-path';
// @ts-ignore
import { SVGPathData, encodeSVGPath } from 'svg-pathdata';
import { Path, Point } from './types';
import memoize from 'lodash/memoize';
function assertNever(x: never): never {
throw new Error('Unknown type: ' + x['type']);
@@ -47,21 +44,17 @@ const extractNodes = (node: INode): INode[] => {
return [];
};
export const getNodes = memoize((src: string) =>
extractNodes(parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`)),
);
export const getNodes = (src: string) =>
extractNodes(parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`));
export const getCommands = (src: string) =>
getNodes(src)
.map(convertToPathNode)
.flatMap(({ d, name }, idx) =>
new SVGPathData(d)
.toAbs()
// @ts-ignore
.commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
);
const getPaths = (src: string) => {
export const getPaths = (src: string) => {
const commands = getCommands(src.includes('<svg') ? src : `<svg>${src}</svg>`);
const paths: Path[] = [];
let prev: Point | undefined = undefined;
@@ -244,7 +237,6 @@ const getPaths = (src: string) => {
break;
}
default: {
// @ts-ignore
assertNever(c);
}
}
@@ -252,7 +244,7 @@ const getPaths = (src: string) => {
return paths;
};
const arcEllipseCenter = (
export const arcEllipseCenter = (
x1: number,
y1: number,
rx: number,
@@ -304,52 +296,5 @@ const arcEllipseCenter = (
M2[1][0] * Cp[0] + M2[1][1] * Cp[1] + V3[1],
];
return {
x: C[0],
y: C[1],
tangentIntersection: intersectTangents(
{ x: x1, y: y1 },
{ x: x2, y: y2 },
{ x: C[0], y: C[1] },
),
};
return { x: C[0], y: C[1] };
};
function getTangentDirection(p: Point, center: Point): Point {
// Tangent is perpendicular to the radius vector (rotate radius 90°)
const dx = p.x - center.x;
const dy = p.y - center.y;
return { x: -dy, y: dx }; // 90° rotation
}
function intersectTangents(start: Point, end: Point, center: Point): Point | null {
const t1 = getTangentDirection(start, center);
const t2 = getTangentDirection(end, center);
// Solve: start + λ * t1 = end + μ * t2
const A = [
[t1.x, -t2.x],
[t1.y, -t2.y],
];
const b = [end.x - start.x, end.y - start.y];
// Compute determinant
const det = A[0][0] * A[1][1] - A[0][1] * A[1][0];
if (Math.abs(det) < 1e-10) {
// Lines are parallel, no intersection
return null;
}
const invDet = 1 / det;
const lambda = (b[0] * A[1][1] - b[1] * A[0][1]) * invDet;
// Intersection point = start + lambda * t1
return {
x: start.x + lambda * t1.x,
y: start.y + lambda * t1.y,
};
}
export default memoize(getPaths);

View File

@@ -1,4 +1,5 @@
import { bundledLanguages, createHighlighter, type ThemeRegistration } from 'shiki';
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
type CodeExampleType = {
title: string;
@@ -9,20 +10,14 @@ type CodeExampleType = {
const getIconCodes = (): CodeExampleType => {
return [
{
language: 'html',
language: 'js',
title: 'Vanilla',
code: `\
<script>
import { createIcons, $CamelCase } from 'lucide';
import { createIcons, icons } from 'lucide';
createIcons({
icons: {
$CamelCase
}
});
</script>
createIcons({ icons });
<i data-lucide="$Name"></i>\
document.body.append('<i data-lucide="$Name"></i>');\
`,
},
{
@@ -107,8 +102,13 @@ import { LucideAngularModule, $PascalCase } from 'lucide-angular';
},
{
language: 'html',
title: 'Icon font',
code: `<div class="icon-$Name"></div>`,
title: 'Icon Font',
code: `<style>
@import ('~lucide-static/font/Lucide.css');
</style>
<div class="icon-$Name"></div>
`,
},
];
};
@@ -118,7 +118,7 @@ export type ThemeOptions =
| { light: ThemeRegistration; dark: ThemeRegistration };
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await createHighlighter({
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});

View File

@@ -1,5 +1,5 @@
import { bundledLanguages, type ThemeRegistration } from 'shiki';
import { createHighlighter } from 'shiki';
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
type CodeExampleType = {
title: string;
@@ -10,11 +10,10 @@ type CodeExampleType = {
const getIconCodes = (): CodeExampleType => {
return [
{
language: 'html',
language: 'js',
title: 'Vanilla',
code: `\
<script>
import { createIcons } from 'lucide';
import { createIcons, icons } from 'lucide';
import { $CamelCase } from '@lucide/lab';
createIcons({
@@ -22,9 +21,8 @@ createIcons({
$CamelCase
}
});
</script>
<i data-lucide="$Name"></i>\
document.body.append('<i data-lucide="$Name"></i>');\
`,
},
{
@@ -121,7 +119,7 @@ export type ThemeOptions =
| { light: ThemeRegistration; dark: ThemeRegistration };
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await createHighlighter({
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});

View File

@@ -1,12 +1,12 @@
import { bundledLanguages, type ThemeRegistration } from 'shiki';
import { createHighlighter } from 'shiki';
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
export type ThemeOptions =
| ThemeRegistration
| { light: ThemeRegistration; dark: ThemeRegistration };
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await createHighlighter({
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});

View File

@@ -1,5 +1,5 @@
import { createLucideIcon } from 'lucide-react/src/lucide-react';
import { type LucideProps, type IconNode } from 'lucide-react/src/types';
import { type LucideProps, type IconNode } from 'lucide-react/src/createLucideIcon';
import { IconEntity } from '../theme/types';
import { renderToStaticMarkup } from 'react-dom/server';
import { IconContent } from './generateZip';

View File

@@ -1,31 +1,23 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import { computed } from 'vue';
const props = defineProps<{
modelValue: string;
id: string;
}>();
modelValue: string
id: string
}>()
const { isDark } = useData();
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(['update:modelValue'])
const value = computed({
get: () => {
if (props.modelValue == null || props.modelValue === 'currentColor') {
return isDark.value ? '#ffffff' : '#000000';
}
return props.modelValue;
},
set: (val) => emit('update:modelValue', val),
});
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
</script>
<template>
<div class="color-picker">
<div class="color-input-wrapper">
<!-- TODO: Add currentColor div if value is currentColor -->
<input
type="color"
:id="id"
@@ -41,7 +33,6 @@ const value = computed({
class="color-input-text"
aria-label="Color picker input"
v-model="value"
placeholder="[default]"
/>
</div>
</template>
@@ -54,21 +45,19 @@ const value = computed({
top: -5px;
left: -5px;
}
.color-input-wrapper {
height: 24px;
width: 24px;
overflow: hidden;
position: relative;
border-radius: 4px;
border-radius: 12px;
flex-shrink: 0;
}
.color-picker {
background: var(--color-picker-bg, var(--vp-c-bg-alt));
border-radius: 8px;
color: var(--vp-c-text-2);
padding: 3px 8px 3px 3px;
padding: 4px 8px;
height: auto;
font-size: 14px;
text-align: left;
@@ -77,10 +66,6 @@ const value = computed({
display: flex;
align-items: center;
gap: 2px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
}
.color-input-text {
@@ -94,18 +79,15 @@ const value = computed({
text-align: left;
border-radius: 8px;
cursor: text;
transition:
border-color 0.25s,
background 0.4s ease;
letter-spacing: 1px;
transition: border-color 0.25s, background 0.4s ease;
}
.color-picker:hover,
.color-picker:focus {
.color-picker:hover, .color-picker:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.color-input[value='currentColor'] {
.color-input[value="currentColor"] {
}
</style>

View File

@@ -1,27 +1,14 @@
<script setup>
import Icon from 'lucide-vue-next/src/Icon';
import { search } from '../../../data/iconNodes';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { search } from '../../../data/iconNodes'
defineProps({
shortcut: {
type: String,
required: false,
},
});
const SearchIcon = createLucideIcon('search', search)
</script>
<template>
<button class="fake-input">
<Icon
:iconNode="search"
class="search-icon"
/>
<slot />
<kbd
v-if="shortcut"
class="shortcut"
>{{ shortcut }}</kbd
>
<component :is="SearchIcon" class="search-icon"/>
<slot/>
</button>
</template>
@@ -39,34 +26,11 @@ defineProps({
cursor: text;
display: flex;
gap: 12px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
}
.fake-input:hover,
.fake-input:focus {
.fake-input:hover, .fake-input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.shortcut {
margin-left: auto;
padding: 2px 6px;
font-size: 12px;
font-family: inherit;
font-weight: 500;
line-height: 1.5;
color: var(--vp-c-text-3);
background: var(--vp-c-default-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
}
@media (hover: none) {
.shortcut {
display: none;
}
}
</style>

View File

@@ -5,6 +5,7 @@
</template>
<style scoped>
.icon-button {
display: inline-flex;
border: 1px solid transparent;
@@ -29,9 +30,9 @@
}
.icon-button:active {
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
border-color: var(--vp-button-alt-active-border);
color: var(--vp-button-alt-active-text);
background-color: var(--vp-button-alt-active-bg);
}
.icon-button.active {

View File

@@ -1,90 +1,44 @@
<script lang="ts">
export default {
inheritAttrs: false,
};
}
export interface InputProps {
type: string;
modelValue: string;
shortcut?: string;
type: string
modelValue: string
}
</script>
<script setup lang="ts">
import { ref, onMounted, nextTick, watch } from 'vue';
import Icon from 'lucide-vue-next/src/Icon';
import { x } from '../../../data/iconNodes';
import IconButton from './IconButton.vue';
import { ref } from 'vue'
const props = withDefaults(defineProps<InputProps>(), {
type: 'text',
});
type: 'text'
})
const input = ref();
const wrapperEl = ref();
const shortcutEl = ref();
const input = ref()
const emit = defineEmits(['change', 'input', 'update:modelValue']);
const updateShortcutSpacing = () => {
nextTick(() => {
if (shortcutEl.value && wrapperEl.value) {
const shortcutWidth = shortcutEl.value.offsetWidth;
wrapperEl.value.style.setProperty('--shortcut-width', `${shortcutWidth}px`);
}
});
};
onMounted(updateShortcutSpacing);
watch(() => props.shortcut, updateShortcutSpacing);
function onClear() {
emit('update:modelValue', '');
input.value.focus();
}
defineEmits(['change', 'input', 'update:modelValue'])
defineExpose({
focus: () => {
input.value.focus();
},
});
input.value.focus()
}
})
</script>
<template>
<div
class="input-wrapper"
ref="wrapperEl"
>
<slot
name="icon"
class="icon"
/>
<div class="input-wrapper">
<slot name="icon" class="icon" />
<input
:type="type"
class="input"
:class="{ 'has-icon': $slots.icon, 'has-shortcut': shortcut }"
:class="{'has-icon': $slots.icon}"
ref="input"
:value="modelValue"
v-bind="$attrs"
@input="$emit('update:modelValue', $event.target.value)"
/>
<IconButton
@click="onClear"
v-if="type === 'search' && modelValue"
class="clear-button"
aria-label="Clear input"
>
<Icon
:iconNode="x"
:size="20"
/>
</IconButton>
<kbd
v-if="shortcut"
class="shortcut"
ref="shortcutEl"
>{{ shortcut }}</kbd
>
</div>
</template>
@@ -92,7 +46,6 @@ defineExpose({
.input-wrapper {
position: relative;
}
.input {
justify-content: flex-start;
border: 1px solid transparent;
@@ -102,18 +55,9 @@ defineExpose({
height: 40px;
background-color: var(--vp-c-bg-alt);
font-size: 14px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
}
.input.has-shortcut {
padding-right: calc(var(--shortcut-width, 40px) + 22px);
}
.input:hover,
.input:focus {
.input:hover, .input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
@@ -122,40 +66,11 @@ defineExpose({
padding-left: 52px;
}
.clear-button {
position: absolute;
right: 56px;
top: 9px;
padding: 4px;
transition: background-color .25s;
}
.shortcut {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
padding: 2px 6px;
font-size: 12px;
font-family: inherit;
font-weight: 500;
line-height: 1.5;
color: var(--vp-c-text-3);
background: var(--vp-c-default-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
pointer-events: none;
}
@media (hover: none) {
.shortcut {
display: none;
}
}
</style>
<style>
.input-wrapper > svg {
.input-wrapper svg {
position: absolute;
left: 16px;
top: 12px;

View File

@@ -1,36 +1,37 @@
<script lang="ts">
export default {
inheritAttrs: false,
};
}
</script>
<script setup lang="ts">
import { computed, ref } from 'vue';
import Input from './Input.vue';
import Icon from 'lucide-vue-next/src/Icon';
import { search } from '../../../data/iconNodes';
import { computed, ref } from 'vue'
import Input from './Input.vue'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { search } from '../../../data/iconNodes'
const SearchIcon = createLucideIcon('search', search)
interface Props {
modelValue: string;
shortcut?: string;
modelValue: string
}
const props = defineProps<Props>();
const props = defineProps<Props>()
const input = ref();
const input = ref()
const emit = defineEmits(['update:modelValue']);
const emit = defineEmits(['update:modelValue'])
defineExpose({
focus: () => {
input.value.focus();
},
});
input.value.focus()
}
})
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
set: (val) => emit('update:modelValue', val)
})
</script>
<template>
@@ -38,16 +39,12 @@ const value = computed({
ref="input"
type="search"
autofocus
:shortcut="shortcut"
v-bind="$attrs"
v-model="value"
class="input-wrapper"
>
<template #icon>
<Icon
:iconNode="search"
class="search-icon"
/>
<component :is="SearchIcon" class="search-icon" />
</template>
</Input>
</template>
@@ -63,8 +60,7 @@ const value = computed({
background-color: var(--vp-c-bg-alt);
}
.input:hover,
.input:focus {
.input:hover, .input:focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
@@ -75,4 +71,5 @@ const value = computed({
font-size: 14px;
height: 48px;
}
</style>

View File

@@ -1,15 +1,14 @@
<script setup lang="ts">
import { rotateCw } from '../../../data/iconNodes';
import Icon from 'lucide-vue-next/src/Icon';
import IconButton from './IconButton.vue';
import { rotateCw } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import IconButton from "./IconButton.vue";
const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
</script>
<template>
<IconButton class="reset-button">
<Icon
:size="20"
:iconNode="rotateCw"
/>
<RotateIcon :size="20"/>
</IconButton>
</template>
@@ -33,7 +32,6 @@ import IconButton from './IconButton.vue';
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}

View File

@@ -1,22 +1,17 @@
<script setup>
import { ref } from 'vue'
import { Switch } from '@headlessui/vue'
defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue'])
const enabled = ref(false)
</script>
<template>
<Switch
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
v-model="enabled"
:key="enabled"
class="switch"
:class="{ enabled: modelValue }"
:class="{ enabled }"
>
<span class="thumb" />
</Switch>

View File

@@ -5,15 +5,10 @@ import LucideIcon from '../base/LucideIcon.vue'
import { useRouter } from 'vitepress';
import { random } from 'lodash-es'
import FakeInput from '../base/FakeInput.vue'
import useSearchShortcut from '../../utils/useSearchShortcut'
const { go } = useRouter()
const intervalTime = shallowRef()
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
go('/icons/?focus')
})
const getInitialItems = () => data.icons.slice(0, 48)
const items = ref(getInitialItems())
let id = items.value.length + 1
@@ -69,11 +64,7 @@ onBeforeUnmount(() => {
</div>
</TransitionGroup>
</div>
<FakeInput
@click="go('/icons/?focus')"
:shortcut="kbdSearchShortcut"
class="search-box"
>
<FakeInput @click="go('/icons/?focus')" class="search-box">
Search {{ data.iconsCount }} icons...
</FakeInput>
</div>

View File

@@ -48,11 +48,12 @@ function resetStyle () {
color.value = 'currentColor'
strokeWidth.value = 2
size.value = 24
absoluteStrokeWidth.value = false
absoluteStrokeWidth.value = false;
}
watch(absoluteStrokeWidth, (enabled) => {
iconContainer.value?.classList.toggle('absolute-stroke-width', enabled)
})
</script>
@@ -122,6 +123,9 @@ watch(absoluteStrokeWidth, (enabled) => {
>
<template #display>
<Switch
:key="absoluteStrokeWidth"
class="switch"
:class="{ enabled: absoluteStrokeWidth }"
id="absolute-stroke-width"
name="absolute-stroke-width"
v-model="absoluteStrokeWidth"

View File

@@ -22,16 +22,16 @@ export default {
logo: '/framework-logos/svelte.svg',
label: 'Lucide documentation for Svelte',
},
{
name: 'lucide-solid',
logo: '/framework-logos/solid.svg',
label: 'Lucide documentation for Solid',
},
{
name: 'lucide-preact',
logo: '/framework-logos/preact.svg',
label: 'Lucide documentation for Preact',
},
{
name: 'lucide-solid',
logo: '/framework-logos/solid.svg',
label: 'Lucide documentation for Solid',
},
{
name: 'lucide-angular',
logo: '/framework-logos/angular.svg',
@@ -48,6 +48,11 @@ export default {
logo: '/framework-logos/react-native.svg',
label: 'Lucide documentation for React Native',
},
{
name: 'lucide-flutter',
logo: '/framework-logos/flutter.svg',
label: 'Lucide documentation for Flutter',
},
],
};
},

View File

@@ -2,48 +2,45 @@
import { useData } from 'vitepress';
import { useSessionStorage } from '@vueuse/core';
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-next/src/Icon';
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
import { x } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { onMounted, ref } from 'vue';
const { theme } = useData();
const showAd = useSessionStorage('show-carbon-ads', true);
const carbonLoaded = ref(true);
const { theme } = useData()
const showAd = useSessionStorage('show-carbon-ads', true)
const carbonLoaded = ref(true)
defineProps<{
drawerOpen: boolean;
}>();
drawerOpen: boolean
}>()
const CloseIcon = createLucideIcon('Close', x)
onMounted(() => {
setTimeout(() => {
if (window?._carbonads == null) {
carbonLoaded.value = false;
carbonLoaded.value = false
}
}, 5000);
});
}, 5000)
})
</script>
<template>
<div
:class="{
'drawer-open': drawerOpen,
'hide-ad': !(showAd && carbonLoaded),
'hide-ad': !(showAd && carbonLoaded)
}"
class="floating-ad"
v-if="theme.carbonAds"
>
<IconButton
@click="showAd = false"
class="hide-button"
>
<Icon
:iconNode="x"
:size="20"
absoluteStrokeWidth
/>
<IconButton @click="showAd = false" class="hide-button">
<component :is="CloseIcon" :size="20" absoluteStrokeWidth />
</IconButton>
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" />
<VPDocAsideCarbonAds
:carbon-ads="theme.carbonAds"
/>
</div>
</template>
@@ -54,9 +51,7 @@ onMounted(() => {
bottom: 32px;
width: 224px;
right: 32px;
transition:
opacity 0.5s,
transform 0.25s ease;
transition: opacity 0.5s, transform 0.25s ease;
}
.floating-ad.drawer-open {
@@ -72,11 +67,8 @@ onMounted(() => {
transform: translateY(-288px) translateX(224px);
}
.floating-ad.drawer-open,
.floating-ad.hide-ad {
transition:
opacity 0.25s,
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
.floating-ad.drawer-open, .floating-ad.hide-ad {
transition: opacity 0.25s, transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
@media (min-width: 1280px) {

View File

@@ -1,68 +1,70 @@
<script setup lang="ts">
import { ref } from 'vue';
import ButtonMenu from '../base/ButtonMenu.vue';
import ButtonMenu from '../base/ButtonMenu.vue'
import { useIconStyleContext } from '../../composables/useIconStyle';
import useConfetti from '../../composables/useConfetti';
import getSVGIcon from '../../utils/getSVGIcon';
import downloadData from '../../utils/downloadData';
const downloadText = 'Download!';
const copiedText = 'Copied!';
const confettiText = ref(copiedText);
const downloadText = 'Download!'
const copiedText = 'Copied!'
const confettiText = ref(copiedText)
const props = defineProps<{
name: string;
popoverPosition?: 'top' | 'bottom';
}>();
name: string
popoverPosition?: 'top' | 'bottom'
}>()
const { size } = useIconStyleContext();
const { size } = useIconStyleContext()
const { animate, confetti } = useConfetti();
const { animate, confetti } = useConfetti()
function copySVG() {
confettiText.value = copiedText;
const svgString = getSVGIcon();
confettiText.value = copiedText
const svgString = getSVGIcon()
navigator.clipboard.writeText(svgString);
navigator.clipboard.writeText(svgString)
confetti();
confetti()
}
function copyDataUrl() {
confettiText.value = copiedText;
const svgString = getSVGIcon();
confettiText.value = copiedText
const svgString = getSVGIcon()
// Create SVG data url
const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
navigator.clipboard.writeText(dataUrl);
const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`
navigator.clipboard.writeText(dataUrl)
confetti();
confetti()
}
function downloadSVG() {
confettiText.value = downloadText;
const svgString = getSVGIcon();
confettiText.value = downloadText
const svgString = getSVGIcon()
downloadData(`${props.name}.svg`, `data:image/svg+xml;base64,${btoa(svgString)}`);
confetti();
downloadData(`${props.name}.svg`, `data:image/svg+xml;base64,${btoa(svgString)}`)
confetti()
}
function downloadPNG() {
confettiText.value = downloadText;
const svgString = getSVGIcon();
confettiText.value = downloadText
const svgString = getSVGIcon()
const canvas = document.createElement('canvas');
canvas.width = size.value;
canvas.height = size.value;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
const image = new Image();
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
image.onload = function () {
image.onload = function() {
ctx.drawImage(image, 0, 0);
downloadData(`${props.name}.png`, canvas.toDataURL('image/png'));
confetti();
};
downloadData(`${props.name}.png`, canvas.toDataURL('image/png'))
confetti()
}
}
</script>
<template>
@@ -73,10 +75,10 @@ function downloadPNG() {
:data-confetti-text="confettiText"
:popoverPosition="popoverPosition"
:options="[
{ text: 'Copy SVG', onClick: copySVG },
{ text: 'Copy Data URL', onClick: copyDataUrl },
{ text: 'Download SVG', onClick: downloadSVG },
{ text: 'Download PNG', onClick: downloadPNG },
{ text: 'Copy SVG' , onClick: copySVG },
{ text: 'Copy Data URL' , onClick: copyDataUrl },
{ text: 'Download SVG' , onClick: downloadSVG },
{ text: 'Download PNG' , onClick: downloadPNG },
]"
/>
</template>

View File

@@ -1,44 +1,41 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import { copy } from '../../../data/iconNodes';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import { copy } from '../../../data/iconNodes'
import useConfetti from '../../composables/useConfetti';
import Icon from 'lucide-vue-next/src/Icon';
const { animate, confetti } = useConfetti();
const slots = useSlots();
const { animate, confetti } = useConfetti()
const slots = useSlots()
const copiedText = computed(() => slots.default?.()[0].children);
const copiedText = computed(() => slots.default?.()[0].children)
function copyText() {
navigator.clipboard.writeText(copiedText.value);
navigator.clipboard.writeText(copiedText.value)
confetti();
confetti()
}
const Copy = createLucideIcon('ChevronUp', copy)
</script>
<template>
<h1
class="icon-name confetti-button"
:class="{ animate }"
:class="{animate}"
data-confetti-text="Copied!"
@click="copyText"
>
<slot />
<Icon
:iconNode="copy"
:size="20"
class="copy-icon"
/>
<Copy :size="20" class="copy-icon"/>
</h1>
</template>
<style scoped>
@import './confetti.css';
.icon-name {
font-size: 24px;
font-weight: 500;
line-height: 32px;
transition: background ease-in 0.15s;
transition: background ease-in .15s;;
padding: 2px 8px;
border-radius: 8px;
width: auto;
@@ -51,7 +48,7 @@ function copyText() {
}
.icon-name:hover .copy-icon {
opacity: 0.9;
opacity: .9;
}
.icon-name:before,
@@ -68,10 +65,10 @@ function copyText() {
opacity: 0;
margin-left: 12px;
margin-top: 6px;
transition: ease 0.3s opacity;
transition:ease .3s opacity;
}
.icon-name:hover .copy-icon:hover {
opacity: 0.6;
opacity: .6;
}
</style>

View File

@@ -9,7 +9,7 @@ import {useData, useRouter} from 'vitepress';
import { computed } from 'vue';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { diamond } from '../../../data/iconNodes'
import deprecationReasonTemplate from '../../../../../tools/build-icons/utils/deprecationReasonTemplate.ts';
import deprecationReasonTemplate from '../../../../../tools/build-icons/utils/deprecationReasonTemplate.mjs';
const props = defineProps<{

View File

@@ -178,8 +178,6 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
stroke-width: var(--customize-strokeWidth, 2);
width: calc(var(--customize-size, 24) * 1px);
height: calc(var(--customize-size, 24) * 1px);
max-width: 3rem;
max-height: 3rem;
}
html.absolute-stroke-width .lucide-icon.customizable {

View File

@@ -26,9 +26,10 @@ const iconComponent = computed(() => {
<component
ref="previewIcon"
:is="iconComponent"
:size="size"
:color="color"
:strokeWidth="absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth"
:width="size"
:height="size"
:stroke="color"
:stroke-width="absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth"
/>
<svg class="icon-grid" :viewBox="`0 0 ${size} ${size}`" fill="none" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg">
<g :key="`grid-${i}`" v-for="(_, i) in gridLines">

View File

@@ -1,19 +1,18 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted } from 'vue';
import { ref, computed, defineAsyncComponent, onMounted, watch, watchEffect } from 'vue';
import type { IconEntity, Category } from '../../types';
import useSearch from '../../composables/useSearch';
import InputSearch from '../base/InputSearch.vue';
import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
import StickyBar from './StickyBar.vue';
import IconsCategory, { CategoryRow } from './IconsCategory.vue';
import IconsCategory from './IconsCategory.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import chunkArray from '../../utils/chunkArray';
import { CategoryRow } from './IconsCategory.vue';
import useScrollToCategory from '../../composables/useScrollToCategory';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
@@ -28,10 +27,6 @@ const activeIconName = ref(null);
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const isSearching = computed(() => !!searchQuery.value);
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
function setActiveIconName(name: string) {
activeIconName.value = name;
}
@@ -40,10 +35,10 @@ const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => {
return Math.floor(containerWidth.value / (ICON_SIZE + ICON_GRID_GAP));
return Math.floor((containerWidth.value) / ((ICON_SIZE + ICON_GRID_GAP)));
});
const mappedIcons = computed(() => {
@@ -71,27 +66,26 @@ const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
const categories = computed(() => {
if (!props.categories?.length || !props.icons?.length) return [];
return props.categories.map(({ name, title }) => {
const categoryIcons = props.icons.filter((icon) => {
const iconCategories = icon?.externalLibrary
? icon.categories
: props.iconCategories[icon.name];
return props.categories
.map(({ name, title }) => {
const categoryIcons = props.icons.filter((icon) => {
const iconCategories = icon?.externalLibrary ? icon.categories : props.iconCategories[icon.name]
return iconCategories?.includes(name);
});
return iconCategories?.includes(name);
});
const searchedCategoryIcons = isSearching
? categoryIcons.filter((icon) =>
searchResults.value.some((item) => item?.name === icon?.name),
)
: categoryIcons;
const searchedCategoryIcons = isSearching
? categoryIcons.filter((icon) =>
searchResults.value.some((item) => item?.name === icon?.name)
)
: categoryIcons;
return {
title,
name,
icons: searchedCategoryIcons,
};
});
return {
title,
name,
icons: searchedCategoryIcons,
};
})
});
const categoriesList = computed(() => {
@@ -108,24 +102,26 @@ const categoriesList = computed(() => {
return acc;
}, []);
});
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10,
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
categoriesList,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10
},
)
useScrollToCategory({
categories,
categoriesList,
scrollTo,
searchQueryDebounced,
});
})
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll);
});
useEventListener(window, 'scroll', containerProps.onScroll)
})
function onFocusSearchInput() {
if (tags.value == null) {
@@ -144,27 +140,28 @@ function handleCloseDrawer() {
window.history.pushState({}, '', '/icons/categories');
}
watchEffect(() => {
console.log(props.icons.find((icon) => icon.name === 'burger'));
});
</script>
<template>
<div
ref="overviewEl"
class="overview-container"
>
<div ref="overviewEl" class="overview-container">
<StickyBar class="category-search">
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
v-if="categories.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<div v-bind="wrapperProps">
@@ -205,4 +202,8 @@ function handleCloseDrawer() {
.icons {
margin-bottom: 8px;
}
.overview-container {
padding-bottom: 288px;
}
</style>

View File

@@ -2,30 +2,19 @@
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import { useRoute } from 'vitepress';
import IconGrid from './IconGrid.vue';
import InputSearch from '../base/InputSearch.vue';
import useSearch from '../../composables/useSearch';
import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
import StickyBar from './StickyBar.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
import chunkArray from '../../utils/chunkArray';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const initialGridItems = computed(() => {
if (containerWidth.value === 0) return 120;
const itemsPerRow = columnSize.value || 10;
const visibleRows = Math.ceil(window.innerHeight / (ICON_SIZE + ICON_GRID_GAP));
return Math.min(itemsPerRow * (visibleRows + 2), 200);
});
const DEFAULT_GRID_ITEMS = 10 * 160;
const props = defineProps<{
icons: IconEntity[];
@@ -37,10 +26,10 @@ const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => {
return Math.floor(containerWidth.value / (ICON_SIZE + ICON_GRID_GAP));
return Math.floor((containerWidth.value) / ((ICON_SIZE + ICON_GRID_GAP)));
});
const mappedIcons = computed(() => {
@@ -61,38 +50,30 @@ const mappedIcons = computed(() => {
});
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
]);
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(chunkedIcons, {
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10,
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
chunkedIcons,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10
},
)
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll);
useEventListener(window, 'scroll', containerProps.onScroll)
})
// Check if we should focus the search input from URL parameter
const route = useRoute();
if (route.data?.relativePath && window.location.search.includes('focus')) {
searchInput.value?.focus();
}
});
function setActiveIconName(name: string) {
activeIconName.value = name;
@@ -112,8 +93,8 @@ const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
watch(searchQueryDebounced, () => {
scrollTo(0);
});
scrollTo(0)
})
function handleCloseDrawer() {
setActiveIconName('');
@@ -123,30 +104,26 @@ function handleCloseDrawer() {
</script>
<template>
<div
ref="overviewEl"
class="overview-container"
>
<div ref="overviewEl" class="overview-container">
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
v-if="list.length === 0 && searchQuery !== ''"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<IconGrid
v-else-if="list.length === 0"
:key="index"
overlayMode
:icons="searchResults.slice(0, initialGridItems)"
:icons="[...searchResults].splice(0, DEFAULT_GRID_ITEMS)"
:activeIcon="activeIconName"
@setActiveIcon="setActiveIconName"
/>
@@ -186,4 +163,8 @@ function handleCloseDrawer() {
.input-wrapper {
width: 100%;
}
.overview-container {
padding-bottom: 288px;
}
</style>

View File

@@ -1,218 +1,56 @@
<script setup lang="ts">
import { ref, onMounted, computed, markRaw, shallowReadonly, watch } from 'vue';
import {
bird,
squirrel,
rabbit,
ghost,
castle,
drama,
dog,
cat,
wandSparkles,
save,
snowflake,
cake,
fish,
turtle,
rat,
worm,
testTubeDiagonal,
sword,
} from '../../../data/iconNodes';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { useEventListener } from '@vueuse/core';
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
import { IconNode } from '../../types';
import { ref, onMounted, computed } from 'vue'
import { bird, squirrel, rabbit } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
import {useEventListener} from '@vueuse/core'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
import { IconNode } from '../../types'
const { searchQuery, isBrandSearch } = defineProps<{
searchQuery: string;
isBrandSearch: boolean;
}>();
defineProps<{
searchQuery: string
}>()
defineEmits(['clear']);
defineEmits(['clear'])
interface Placeholder {
title: string;
message: string;
icon: IconNode;
finePrint?: string;
}
const brandPlaceholders: Placeholder[] = shallowReadonly([
{
title: 'Boooo! What a scary brand logo!',
message:
'[name] and its friends often haunt this search box, but you wont ever find them here.',
icon: markRaw(ghost),
},
{
title: 'Thank You Mario!',
message: 'But [name] is in another castle!',
icon: markRaw(castle),
},
{
title: '[name] did audition for our icon set',
message: '...but didnt make the callback.',
icon: markRaw(drama),
},
{
title: 'Such Search. Very [name].',
message: 'Much not here. So Wow.',
icon: markRaw(dog),
},
{
title: 'I Can Has [name]?',
message: 'No [name] for you in here.',
icon: markRaw(cat),
},
{
title: 'Loading [name]...',
message: 'Fatal error: our cartridge contains only open-source pixels.',
icon: markRaw(save),
},
{
title: '[name] Shall Not Pass',
message: 'Do not look to its coming at first light of any day.',
icon: markRaw(wandSparkles),
},
{
title: 'Winter is coming',
message: 'But [name] sure isnt.',
icon: markRaw(snowflake),
},
{
title: 'The cake is a lie',
message: 'And so is the promise of an icon for [name] at Lucide.',
icon: markRaw(cake),
},
{
title: 'Its not a bug',
message: 'Having no [name] icon around is a feature.',
icon: markRaw(worm),
},
{
title: 'The lab exploded',
message: 'We tried mixing [name] with open-source icons.',
icon: markRaw(testTubeDiagonal),
},
{
title: 'Its Dangerous to Go Alone',
message: 'Take this icon instead — its not [name], but it might help.',
icon: markRaw(sword),
},
]);
const notFoundPlaceholders: Omit<Placeholder, 'title'>[] = shallowReadonly([
{
message: 'Weve looked for this icon for a birds eye view, but could not find it.',
icon: markRaw(bird),
},
{
message: 'We checked every tree. Only acorns, no results.',
icon: markRaw(squirrel),
},
{
message: 'Youve gone too deep into the rabbit hole — this icon doesnt exist.',
icon: markRaw(rabbit),
},
{
message: 'This icon seems to have slipped through the net.',
icon: markRaw(fish),
},
{
message: 'This icon might exist in the future… but it hasnt arrived yet.',
icon: markRaw(turtle),
},
{
message: 'Rats! This icon seems to have slipped through the cracks.',
icon: markRaw(rat),
},
]);
function randomItem<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
}
const placeholderIcon = ref<HTMLElement>();
const placeholder = ref<Placeholder>();
watch(
() => isBrandSearch,
() => {
placeholder.value = isBrandSearch
? {
...randomItem(brandPlaceholders),
finePrint:
'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.',
}
: {
title: `No results for “[name]”`,
finePrint:
'This icon doesnt seem to exist… yet. Try searching similar terms, browsing existing requests, or opening a new one.',
...randomItem(notFoundPlaceholders),
};
},
{ immediate: true },
);
const iconComponent = computed(() => createLucideIcon('placeholder', placeholder.value.icon));
const flip = ref(false);
const animalIcon = ref<HTMLElement>()
const randomAnimal = computed<IconNode>(() => {
return Math.random() > 0.5 ? squirrel : Math.random() > 0.5 ? rabbit : bird
})
const animalComponent = computed(() => createLucideIcon('animal', randomAnimal.value))
const flip = ref(false)
onMounted(() => {
useEventListener(document, 'mousemove', (mouseEvent) => {
const { width, x } = placeholderIcon.value.getBoundingClientRect();
const {width, height, x, y} = animalIcon.value.getBoundingClientRect()
const centerX = width / 2 + x;
const centerX = (width / 2) + x
flip.value = mouseEvent.x < centerX
})
})
flip.value = !isBrandSearch && mouseEvent.x < centerX;
});
});
</script>
<template>
<div class="no-results">
<component
:is="iconComponent"
class="placeholder-icon"
ref="placeholderIcon"
:is="animalComponent"
class="animal-icon"
ref="animalIcon"
:class="{ flip }"
:strokeWidth="1"
/>
<h2 class="no-results-text">{{ placeholder.title.replace('[name]', searchQuery) }}</h2>
<p class="no-results-message">
{{ placeholder.message.replace('[name]', searchQuery) }}
</p>
<div class="divider"></div>
<p
v-if="placeholder.finePrint"
class="no-results-fine-print"
>
{{ placeholder.finePrint }}
</p>
<h2 class="no-results-text">
No icons found for '{{ searchQuery }}'
</h2>
<VPButton
v-if="isBrandSearch"
text="Head over to Simple Icons"
theme="brand"
:href="`https://simpleicons.org/?q=${searchQuery}`"
target="_blank"
/>
<VPButton
v-else
text="Clear search & try again"
theme="brand"
text="Clear your search and try again"
theme="alt"
@click="$emit('clear')"
/>
<span class="text-divider">or</span>
<VPButton
v-if="isBrandSearch"
text="Read our statement on brand logos"
theme="alt"
href="https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md"
target="_blank"
/>
<VPButton
v-else
text="Search GitHub issues"
text="Search on Github issues"
theme="alt"
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
target="_blank"
@@ -225,38 +63,33 @@ onMounted(() => {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-block: 48px;
}
.placeholder-icon {
width: 96px;
height: 96px;
color: var(--vp-c-text-1);
.animal-icon {
width: 160px;
height: 160px;
color: var(--vp-c-neutral);
opacity: 0.8;
margin-top: 72px;
}
.placeholder-icon.flip {
.animal-icon.flip {
transform: rotateY(180deg);
}
@media (min-width: 960px) {
.animal-icon {
width: 240px;
height: 240px;
}
}
.no-results-text {
line-height: 1.35;
line-height: 40px;
font-size: 24px;
margin-top: 24px;
margin-bottom: 8px;
text-wrap: balance;
}
.no-results-message {
text-wrap: balance;
}
.no-results-fine-print {
max-inline-size: 60ch;
font-size: 14px;
margin-bottom: 32px;
color: var(--vp-c-text-2);
text-wrap: balance;
text-align: center;
}
.text-divider {
@@ -264,10 +97,4 @@ onMounted(() => {
font-size: 16px;
color: var(--vp-c-neutral);
}
.divider {
margin: 24px auto 18px;
width: 64px;
height: 1px;
background-color: var(--vp-c-divider);
}
</style>

View File

@@ -1,72 +1,76 @@
<script setup lang="ts">
import { shallowRef, type Ref, watch, computed } from 'vue';
import { useCssVar, syncRef } from '@vueuse/core';
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle';
import RangeSlider from '../base/RangeSlider.vue';
import InputField from '../base/InputField.vue';
import ColorPicker from '../base/ColorPicker.vue';
import ResetButton from '../base/ResetButton.vue';
import Switch from '../base/Switch.vue';
import { shallowRef, type Ref, watch, computed } from 'vue'
import { useCssVar, syncRef } from '@vueuse/core'
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle'
import RangeSlider from '../base/RangeSlider.vue'
import InputField from '../base/InputField.vue'
import ColorPicker from '../base/ColorPicker.vue'
import ResetButton from '../base/ResetButton.vue'
import Switch from '../base/Switch.vue'
const props = defineProps<{
rootEl?: Ref<HTMLElement>;
}>();
rootEl?: Ref<HTMLElement>
}>()
const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext();
const documentRef = shallowRef<HTMLElement | undefined>(
typeof document !== 'undefined' ? document?.documentElement : undefined,
);
const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext()
const documentRef = shallowRef<HTMLElement | undefined>(typeof document !== 'undefined' ? document?.documentElement : undefined)
const colorCssVar = useCssVar('--customize-color', props.rootEl?.value ?? documentRef.value, {
initialValue: `${STYLE_DEFAULTS.color}`,
});
const colorCssVar = useCssVar(
'--customize-color',
props.rootEl?.value ?? documentRef.value,
{
initialValue: `${STYLE_DEFAULTS.color}`
}
)
const strokeWidthCssVar = useCssVar(
'--customize-strokeWidth',
props.rootEl?.value ?? documentRef.value,
{
initialValue: `${STYLE_DEFAULTS.strokeWidth}`,
},
);
initialValue: `${STYLE_DEFAULTS.strokeWidth}`
}
)
const sizeCssVar = useCssVar('--customize-size', props.rootEl?.value ?? documentRef.value, {
initialValue: `${STYLE_DEFAULTS.size}`,
});
const sizeCssVar = useCssVar(
'--customize-size',
props.rootEl?.value ?? documentRef.value,
{
initialValue: `${STYLE_DEFAULTS.size}`
}
)
syncRef(color, colorCssVar, { direction: 'ltr' });
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' });
syncRef(size, sizeCssVar, { direction: 'ltr' });
syncRef(color, colorCssVar, { direction: 'ltr' })
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' })
syncRef(size, sizeCssVar, { direction: 'ltr' })
function resetStyle() {
color.value = STYLE_DEFAULTS.color;
strokeWidth.value = STYLE_DEFAULTS.strokeWidth;
size.value = STYLE_DEFAULTS.size;
absoluteStrokeWidth.value = STYLE_DEFAULTS.absoluteStrokeWidth;
function resetStyle () {
color.value = STYLE_DEFAULTS.color
strokeWidth.value = STYLE_DEFAULTS.strokeWidth
size.value = STYLE_DEFAULTS.size
absoluteStrokeWidth.value = STYLE_DEFAULTS.absoluteStrokeWidth
}
watch(absoluteStrokeWidth, (enabled) => {
const htmlEl = document.documentElement;
const htmlEl = document.documentElement
htmlEl.classList.toggle('absolute-stroke-width', enabled);
});
htmlEl.classList.toggle('absolute-stroke-width', enabled)
})
const customizingActive = computed(() => {
return (
color.value !== STYLE_DEFAULTS.color ||
strokeWidth.value !== STYLE_DEFAULTS.strokeWidth ||
size.value !== STYLE_DEFAULTS.size ||
absoluteStrokeWidth.value !== STYLE_DEFAULTS.absoluteStrokeWidth
);
});
return color.value !== STYLE_DEFAULTS.color
|| strokeWidth.value !== STYLE_DEFAULTS.strokeWidth
|| size.value !== STYLE_DEFAULTS.size
})
</script>
<template>
<div
class="customizer-card"
:class="{ customized: customizingActive }"
>
<div class="customizer-card" :class="{ customized: customizingActive }">
<div class="card-header">
<h2 class="card-title">Customizer</h2>
<h2 class="card-title">
Customizer
</h2>
<ResetButton @click="resetStyle"></ResetButton>
</div>
<InputField
@@ -74,11 +78,7 @@ const customizingActive = computed(() => {
label="Color"
>
<template #display>
<ColorPicker
v-model="color"
id="icon-color"
class="color-picker"
/>
<ColorPicker v-model="color" id="icon-color" class="color-picker"/>
</template>
</InputField>
@@ -111,20 +111,25 @@ const customizingActive = computed(() => {
name="size"
v-model="size"
:min="16"
:max="256"
:max="48"
:step="4"
/>
</InputField>
<InputField
id="absolute-stroke-width"
label="Absolute stroke width"
label="Absolute Stroke width"
>
<Switch
id="absolute-stroke-width"
name="absolute-stroke-width"
:key="absoluteStrokeWidth"
class="switch"
:class="{ enabled: absoluteStrokeWidth }"
v-model="absoluteStrokeWidth"
/>
</InputField>
</div>
</template>
@@ -144,7 +149,6 @@ const customizingActive = computed(() => {
font-size: 16px;
/* margin-bottom: 12px; */
}
.customizer-card {
background: var(--vp-c-bg);
padding: 12px 24px 24px;
@@ -153,7 +157,7 @@ const customizingActive = computed(() => {
position: relative;
z-index: 0;
border: 1px solid transparent;
transition: border-color 0.4s ease-in-out;
transition: border-color .4s ease-in-out;
}
.customizer-card.customized {
@@ -163,4 +167,9 @@ const customizingActive = computed(() => {
.color-picker {
margin-left: auto;
}
#absolute-stroke-width {
flex-direction: row-reverse;
}
</style>

View File

@@ -2,7 +2,7 @@
import { ref, inject, Ref } from 'vue';
export const ICON_STYLE_CONTEXT = Symbol('style');
export const ICON_STYLE_CONTEXT = Symbol('size');
interface IconSizeContext {
size: Ref<number>;

View File

@@ -25,7 +25,6 @@
--vp-code-editor-string: #032f62;
--vp-c-text-4: rgba(60, 60, 67, 0.32);
--vp-home-hero-name-color: var(--vp-c-text);
}
.dark {
@@ -61,15 +60,36 @@
.VPHomeHero .image-container {
transform: none;
width: 100%;
/* padding: 0 24px; */
}
.VPHomeHero .name::first-line {
/* .VPHomeHero .container {
flex-direction: column-reverse;
} */
.VPHomeHero .container .main {
/* flex:1; */
flex-shirk: 0;
}
.VPHomeHero .container .main h1.name {
color: var(--vp-c-text);
}
.VPHomeHero .container .main h1.name .clip {
color: inherit;
-webkit-text-fill-color: unset;
color: var(--vp-c-text);
font-size: 36px;
}
.VPHomeHero .container .main h1::first-line {
color: var(--vp-c-brand);
}
/* */
.VPHomeHero .container .image {
margin: 0;
order: 2;
/* flex: 1; */
margin-top: 32px;
}
@@ -78,6 +98,10 @@
justify-content: flex-end;
}
.VPHomeHero .container .image-bg {
display: none;
}
.VPFeature .icon {
background-color: var(--vp-c-bg);
}
@@ -91,6 +115,12 @@
padding-left: 0;
}
@media (min-width: 640px) {
.VPHomeHero .container .main h1.name .clip {
font-size: unset;
}
}
@media (min-width: 960px) {
.VPHomeHero .container .image {
order: 1;
@@ -110,11 +140,18 @@
.VPHomeHero .container .image-container {
display: block;
}
.VPHomeHero .container .main h1.name {
}
}
.VPNavBarHamburger .container > span {
border-radius: 2px;
}
/*
html:has(* .outline-link:target) {
scroll-behavior: smooth;
} */
.sp-wrapper + * {
margin-top: 24px;
@@ -146,6 +183,7 @@
font-size: 14px;
font-weight: 500;
position: relative;
/* box-sizing: content-box; */
}
.sp-wrapper .sp-tabs .sp-tab-button:after {

View File

@@ -1,41 +0,0 @@
import { ref, Ref, watch } from 'vue';
import BRAND_STOPWORDS from '../../data/brandStopwords.json' with { type: 'json' };
export default function useSearchPlaceholder(
searchQuery: Ref<string, string>,
results: Ref<{ name: string }[]>,
) {
const state = ref({
isNoResults: false,
isBrand: false,
query: '',
});
watch(
results,
() => {
const query = searchQuery.value;
const searchResults = results.value;
if (query.length > 0 && searchResults.length === 0) {
for (const stopword of Object.keys(BRAND_STOPWORDS)) {
if (stopword.startsWith(query)) {
state.value = {
isNoResults: true,
isBrand: true,
query: BRAND_STOPWORDS[stopword],
};
return;
}
}
}
state.value = {
isNoResults: query in BRAND_STOPWORDS && searchResults.length === 0 && query !== '',
isBrand: query in BRAND_STOPWORDS,
query: BRAND_STOPWORDS[query] ?? query,
};
},
{ immediate: true },
);
return state;
}

View File

@@ -1,35 +0,0 @@
import { ref, onMounted, onBeforeUnmount } from 'vue';
/**
* Composable for handling search keyboard shortcuts.
* Listens for Cmd/Ctrl+K and "/" keys to trigger a search action.
*
* @param callback - Function to execute when shortcut is triggered
* @returns Object containing the platform-specific shortcut display text
*/
export default function useSearchShortcut(callback: () => void) {
const shortcutText = ref('');
function handleKeydown(event: KeyboardEvent) {
// Check for Cmd+K (Mac), Ctrl+K (Windows/Linux), or forward slash
if (((event.metaKey || event.ctrlKey) && event.key === 'k') || event.key === '/') {
event.preventDefault();
callback();
}
}
onMounted(() => {
// Detect platform and set appropriate keyboard shortcut for search focus
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
shortcutText.value = isMac ? '⌘K' : 'Ctrl+K';
// Add keyboard shortcut listener
window.addEventListener('keydown', handleKeydown);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown);
});
return { shortcutText };
}

View File

@@ -1,4 +1,4 @@
import { type IconNode } from 'lucide-vue-next/src/types';
import { IconNode } from 'lucide-vue-next/src/createLucideIcon';
import Vue from 'vue';
declare module '*.vue' {
@@ -20,6 +20,5 @@ declare module 'node:module' {
}
declare module '*.node.json' {
const value: IconNode;
export default value;
export default IconNode;
}

View File

@@ -139,22 +139,22 @@ contained icon.
```tsx
// Don't do this
<button class="btn-icon">
<House/>
<Home/>
</button>
// Don't do this either
<button class="btn-icon">
<House aria-label="Home icon"/>
<Home aria-label="Home icon"/>
</button>
// This works but might not be the best solution, see below
<button aria-label="Go to home" class="btn-icon">
<House/>
<Home/>
</button>
// Do this instead
<button class="btn-icon">
<House/>
<Home/>
<span class="visually-hidden">Go to home</span>
</button>
```
@@ -167,7 +167,6 @@ Although you could provide accessible labels to your elements via the `aria-labe
generally recommend avoiding this and instead suggest that you use your chosen CSS framework's "
visually hidden" utility whenever possible. You can
[read more about why `aria-label` might not be the best solution](https://gomakethings.com/revisting-aria-label-versus-a-visually-hidden-class/).
### Example - Radix UI
Use [Radix UI's built-in accessible icon utility component](https://www.radix-ui.com/primitives/docs/utilities/accessible-icon).
@@ -180,7 +179,6 @@ import { AccessibleIcon } from '@radix-ui/react-accessible-icon';
<ArrowRightIcon />
</AccessibleIcon>
```
### Example - Bootstrap
```html

View File

@@ -7,9 +7,9 @@ Beside aliases names lucide also includes prefixed and suffixed names to use wit
```tsx
// These are all the same icon
import {
House,
HouseIcon,
LucideHouse,
Home,
HomeIcon,
LucideHome,
} from "lucide-react";
```

View File

@@ -42,11 +42,7 @@ Here are rules that should be followed to keep quality and consistency when maki
### 8. Distinct elements must have **2 pixels of spacing between each other**
![2px-element-spacing](../../images/2px-element-spacing.svg?raw=true '2px-element-spacing')
![2px-element-spacing-connected](../../images/2px-element-spacing-connected.svg?raw=true '2px-element-spacing-connected')
![2px-element-spacing-abrupt-cut](../../images/2px-element-spacing-abrupt-cut.svg?raw=true '2px-element-spacing-abrupt-cut')
![2px-element-spacing](../../images/2px-element-spacing.svg?raw=true "2px-element-spacing")
### 9. Icons should have a similar optical volume to `circle` and `square`.

View File

@@ -7,7 +7,7 @@ nextPage:
# What is Lucide?
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](/packages).
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](/packages) to make it easier to use these icons in your project.
## Available Icons

View File

@@ -11,7 +11,7 @@ Implementation of the lucide icon library for web applications.
::: code-group
```sh [pnpm]
pnpm add lucide
pnpm install lucide
```
```sh [yarn]
@@ -37,7 +37,7 @@ Implementation of the lucide icon library for React applications.
::: code-group
```sh [pnpm]
pnpm add lucide-react
pnpm install lucide-react
```
```sh [yarn]
@@ -64,7 +64,7 @@ Implementation of the lucide icon library for Vue applications.
::: code-group
```sh [pnpm]
pnpm add lucide-vue-next
pnpm install lucide-vue-next
```
```sh [yarn]
@@ -91,7 +91,7 @@ Implementation of the lucide icon library for Svelte applications.
::: code-group
```sh [pnpm]
pnpm add lucide-svelte
pnpm install lucide-svelte
```
```sh [yarn]
@@ -117,7 +117,7 @@ Implementation of the lucide icon library for Solid applications.
::: code-group
```sh [pnpm]
pnpm add lucide-solid
pnpm install lucide-solid
```
```sh [yarn]
@@ -143,7 +143,7 @@ Implementation of the lucide icon library for Angular applications.
::: code-group
```sh [pnpm]
pnpm add lucide-angular
pnpm install lucide-angular
```
```sh [yarn]
@@ -169,7 +169,7 @@ Implementation of the lucide icon library for preact applications.
::: code-group
```sh [pnpm]
pnpm add lucide-preact
pnpm install lucide-preact
```
```sh [yarn]
@@ -196,7 +196,7 @@ Implementation of the lucide icon library for Astro applications.
::: code-group
```sh [pnpm]
pnpm add @lucide/astro
pnpm install @lucide/astro
```
```sh [yarn]
@@ -222,7 +222,7 @@ Implementation of the lucide icon library for multiple usages that like to use:
::: code-group
```sh [pnpm]
pnpm add lucide-static
pnpm install lucide-static
```
```sh [yarn]

View File

@@ -1,20 +1,13 @@
# Lucide 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
Implementation of the lucide icon library for Angular applications.
## Installation
::: code-group
```sh [pnpm]
pnpm add lucide-angular
pnpm install lucide-angular
```
```sh [yarn]
@@ -38,11 +31,11 @@ bun add lucide-angular
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';
import { LucideAngularModule, File, Home, Menu, UserCheck } from 'lucide-angular';
@NgModule({
imports: [
LucideAngularModule.pick({File, House, Menu, UserCheck})
LucideAngularModule.pick({File, Home, Menu, UserCheck})
]
})
export class AppModule { }
@@ -71,7 +64,7 @@ Within your templates you may now use one of the following component tags to ins
```html
<lucide-angular name="file" class="my-icon"></lucide-angular>
<lucide-icon name="house" class="my-icon"></lucide-icon>
<lucide-icon name="home" class="my-icon"></lucide-icon>
<i-lucide name="menu" class="my-icon"></i-lucide>
<span-lucide name="user-check" class="my-icon"></span-lucide>
```
@@ -95,7 +88,7 @@ You can pass additional props to adjust the icon appearance.
| `absoluteStrokeWidth` | *boolean* | false |
```html
<i-lucide name="house" [size]="48" color="red" [strokeWidth]="1"></i-lucide>
<i-lucide name="home" [size]="48" color="red" [strokeWidth]="1"></i-lucide>
```
### Global configuration
@@ -158,11 +151,11 @@ They can be used in the same way as the official icons.
```js
import { LucideAngularModule } from 'lucide-angular';
import { coconut } from '@lucide/lab';
import { burger } from '@lucide/lab';
@NgModule({
imports: [
LucideAngularModule.pick({ coconut })
LucideAngularModule.pick({ burger })
]
})
export class AppModule { }

View File

@@ -1,13 +1,6 @@
# Lucide Astro
Astro components for Lucide icons that work perfectly with Astro's island architecture and multi-framework support. Each icon is an Astro component that renders as an inline SVG, providing excellent performance for static sites and server-side rendering scenarios.
**What you can accomplish:**
- Use icons as Astro components with zero JavaScript runtime overhead
- Build fast, static websites with optimized SVG icons
- Integrate seamlessly with Astro's component islands and partial hydration
- Create multi-framework applications where icons work across different UI libraries
- Optimize performance with direct icon imports and build-time rendering
Implementation of the lucide icon library for Astro applications.
## Installation
@@ -100,7 +93,7 @@ The package includes type definitions for all icons. This is useful if you want
```astro
---
import { House, Library, Cog, type Icon as IconType } from '@lucide/astro';
import { Home, Library, Cog, type Icon as IconType } from '@lucide/astro';
type MenuItem = {
name: string;
@@ -112,7 +105,7 @@ const menuItems: MenuItem[] = [
{
name: 'Home',
href: '/',
icon: House,
icon: Home,
},
{
name: 'Blog',

View File

@@ -4,21 +4,14 @@ title: Lucide Preact
# Lucide Preact
Preact components for Lucide icons that provide React-like development experience with a smaller footprint. Each icon is a lightweight Preact component that renders as an inline SVG, perfect for applications that need React compatibility with minimal bundle size.
**What you can accomplish:**
- Use icons as Preact components with React-like syntax and patterns
- Build lightweight applications with Preact's smaller runtime
- Create fast, responsive interfaces with minimal JavaScript overhead
- Maintain React compatibility while reducing bundle size
- Integrate with existing Preact applications and component libraries
Implementation of the lucide icon library for preact applications.
## Installation
::: code-group
```sh [pnpm]
pnpm add lucide-preact
pnpm install lucide-preact
```
```sh [yarn]
@@ -91,10 +84,10 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```jsx
import { Icon } from 'lucide-preact';
import { coconut } from '@lucide/lab';
import { burger } from '@lucide/lab';
const App = () => (
<Icon iconNode={coconut} />
<Icon iconNode={burger} />
);
```
@@ -126,7 +119,7 @@ export default Icon;
import Icon from './Icon';
const App = () => {
return <Icon name="house" />;
return <Icon name="home" />;
};
export default App;

View File

@@ -1,13 +1,6 @@
# Lucide React Native
React Native components for Lucide icons that work seamlessly across iOS and Android platforms. Built on top of react-native-svg, each icon renders as a native SVG component, providing consistent visual appearance and performance across mobile devices.
**What you can accomplish:**
- Use icons as React Native components with platform-consistent rendering
- Build cross-platform mobile apps with scalable vector icons
- Create responsive interfaces that adapt to different screen densities
- Integrate with React Native's styling system and animation libraries
- Maintain consistent icon appearance across iOS and Android platforms
Implementation of the lucide icon library for React Native applications
## Installation
@@ -16,7 +9,7 @@ First, ensure that you have `react-native-svg` (version between 12 and 15) insta
::: code-group
```sh [pnpm]
pnpm add lucide-react-native
pnpm install lucide-react-native
```
```sh [yarn]
@@ -85,10 +78,10 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```jsx
import { Icon } from 'lucide-react-native';
import { coconut } from '@lucide/lab';
import { burger } from '@lucide/lab';
const App = () => (
<Icon iconNode={coconut} />
<Icon iconNode={burger} />
);
```
@@ -120,7 +113,7 @@ export default Icon;
import Icon from './Icon';
const App = () => {
return <Icon name="house" />;
return <Icon name="home" />;
};
export default App;

View File

@@ -1,20 +1,13 @@
# Lucide React
React components for Lucide icons that integrate seamlessly into your React applications. Each icon is a fully-typed React component that renders as an optimized inline SVG, giving you the flexibility of components with the performance of vector graphics.
**What you can accomplish:**
- Import icons as React components with full TypeScript support
- Pass props to customize size, color, stroke width, and other SVG attributes
- Use icons in JSX with the same ease as any other React component
- Benefit from automatic tree-shaking to include only the icons you use
- Create dynamic icon components that respond to state and user interactions
Implementation of the lucide icon library for react applications
## Installation
::: code-group
```sh [pnpm]
pnpm add lucide-react
pnpm install lucide-react
```
```sh [yarn]
@@ -85,16 +78,17 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```jsx
import { Icon } from 'lucide-react';
import { coconut } from '@lucide/lab';
import { burger } from '@lucide/lab';
const App = () => (
<Icon iconNode={coconut} />
<Icon iconNode={burger} />
);
```
## Dynamic Icon Component
It is possible to create one generic icon component to load icons. But it is not recommended, since it is importing all icons during the build. This increases build time and the different modules it will create.
It is possible to create one generic icon component to load icons, but it is not recommended.
Since it is importing all icons during build. This increases build time and the different modules it will create.
`DynamicIcon` is useful for applications that want to show icons dynamically by icon name. For example, when using a content management system with where icon names are stored in a database.

View File

@@ -1,20 +1,13 @@
# Lucide Solid
SolidJS components for Lucide icons that leverage Solid's fine-grained reactivity system. Each icon is a reactive Solid component that renders as an inline SVG, providing exceptional performance through Solid's compile-time optimizations and reactive primitives.
**What you can accomplish:**
- Use icons as SolidJS components with fine-grained reactivity
- Create highly performant interfaces with Solid's reactive system
- Build dynamic icon components that respond to signals and stores
- Integrate seamlessly with Solid's JSX and component patterns
- Optimize performance with direct icon imports and minimal runtime overhead
Implementation of the lucide icon library for solid applications.
## Installation
::: code-group
```sh [pnpm]
pnpm add lucide-solid
pnpm install lucide-solid
```
```sh [yarn]
@@ -98,7 +91,7 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```jsx
import { Icon } from 'lucide-solid';
import { sausage } from '@lucide/lab';
import { burger, sausage } from '@lucide/lab';
const App = () => (
<Icon iconNode={sausage} color="red"/>

View File

@@ -1,51 +1,28 @@
# Lucide Static
Static assets and utilities for Lucide icons that work without JavaScript frameworks. This package provides multiple formats including individual SVG files, SVG sprites, icon fonts, and Node.js utilities for server-side rendering and static site generation.
This package include the following lucide implementations:
**What you can accomplish:**
- Use individual SVG files as images or CSS background images
- Implement icon fonts for CSS-based icon systems
- Create SVG sprites for efficient icon loading in static sites
- Import SVG strings in Node.js applications and server-side rendering
- Build static websites and applications without JavaScript framework dependencies
This package includes the following implementations of Lucide icons:
- Individual SVG files
- All SVG files
- SVG sprite
- Icon font files
- A JavaScript library exporting SVG strings
- Icon fonts
- JavaScript library containing strings of SVGs.
## Who is this for?
## Why lucide-static?
`lucide-static` is suitable for _very specific use cases_ where you want to use Lucide icons without relying on a JavaScript framework or component system. It's ideal for:
This package is suitable for specific use cases, for example if you want to use icon fonts, SVG sprites, normal SVGs or Common.js SVG strings in your javascript project.
- Projects that use icon fonts with plain CSS or utility-first frameworks
- Embedding raw SVG files or sprites directly in HTML
- Using SVGs as CSS background images
- Importing SVG strings into Node.js (CommonJS) environments
::: danger
### Not recommended for production {#production-warning}
SVG sprites and icon fonts include **all icons**, which can significantly increase your app's bundle size and load time.
For production environments, we recommend using a bundler with tree-shaking support to include only the icons you actually use. Consider using:
- [lucide](lucide)
- [lucide-react](lucide-react)
- [lucide-vue](lucide-vue)
- [lucide-vue-next](lucide-vue-next)
- [lucide-angular](lucide-angular)
- [lucide-preact](lucide-preact)
::: warning
While they can be useful for prototyping, it is not recommended to use the SVG sprites or icon fonts provided by this package in production web apps as all the available icons are included in the app, hence increasing loading time and data usage. We recommend to use a bundler and tree-shaking to make sure only the icons you use are bundled with your app. Tree-shaking is only available in these packages: [lucide](lucide), [lucide-react](lucide-react), [lucide-vue](lucide-vue), [lucide-vue-next](lucide-vue-next), [lucide-angular](lucide-angular), [lucide-preact](lucide-preact)
:::
## Installation
## Package Managers
::: code-group
```sh [pnpm]
pnpm add lucide-static
pnpm install lucide-static
```
```sh [yarn]
@@ -62,86 +39,64 @@ bun add lucide-static
:::
## SVG Files
### CDN
You can use standalone SVG files or SVG sprites in several ways.
```html
<!-- SVG file for a single icon -->
<img src="https://unpkg.com/lucide-static@latest/icons/house.svg" />
Check out our [codesandbox example](https://codesandbox.io/s/using-the-svg-sprite-lz1kk).
<!-- Icon font -->
<style>
@font-face {
font-family: 'LucideIcons';
src: url(https://unpkg.com/lucide-static@latest/font/Lucide.ttf) format('truetype');
}
</style>
```
### SVG file as image
## Usage
#### In HTML:
Check out the [codesandbox examples](https://codesandbox.io/s/using-the-svg-sprite-lz1kk).
::: code-group
### SVG Files
```html [Webpack]
#### SVG file as image
To use it in for example html:
```html
<!-- SVG file for a single icon -->
<img src="~lucide-static/icons/house.svg" />
```
```html [CDN]
<!-- SVG file for a single icon -->
<img src="https://unpkg.com/lucide-static@latest/icons/house.svg" />
```
:::
#### In CSS:
::: code-group
```css [Webpack]
```css
.house-icon {
background-image: url(~lucide-static/icons/house.svg);
}
```
```css [CDN]
.house-icon {
background-image: url(https://unpkg.com/lucide-static@latest/icons/house.svg);
}
```
Make sure you have the correct webpack loaders to make this work. [url-loader](https://v4.webpack.js.org/loaders/url-loader/)
:::
#### SVG file as string
Make sure you have the correct Webpack loader configured, such as [`url-loader`](https://v4.webpack.js.org/loaders/url-loader/).
You can simply import each SVG by targeting `lucide-static/icons/{icon-name}.svg`.
To use SVGs in your project you can for example use a [SVG loader](https://v4.webpack.js.org/loaders/svg-inline-loader/).
### SVG file as string
To import an SVG as a string (e.g., for templating):
::: code-group
```js [Webpack]
```js
import arrowRightIcon from 'lucide-static/icons/arrow-right';
// return string of an SVG
```
```js [Vite]
import arrowRightIcon from 'lucide-static/icons/arrow-right.svg?raw';
```
### SVG Sprite
:::
You'll need an SVG loader like [`svg-inline-loader`](https://v4.webpack.js.org/loaders/svg-inline-loader/).
### Using the SVG sprite
:::danger
[Not intended for production use.](#production-warning)
:::
You may also need an additional SVG loader to handle this.
#### Basic sprite usage (not production-optimized):
You may need additional loader for this.
```html
<!-- Icon Sprite, not recommended for production! -->
<img src="lucide-static/sprite.svg#house" />
```
#### Inline usage:
```html
<!-- or -->
<svg
width="24"
height="24"
@@ -154,13 +109,12 @@ You may also need an additional SVG loader to handle this.
<use href="#alert-triangle" />
</svg>
<!-- sprite SVG -->
<svg>...</svg>
<svg>
...sprite svg
</svg>
```
#### Inline with CSS helper class
If you'd prefer, you can use CSS to hold your base SVG properties:
If you'd prefer, you can use CSS to hold your base SVG properties
```css
.lucide-icon {
@@ -174,79 +128,47 @@ If you'd prefer, you can use CSS to hold your base SVG properties:
}
```
...and update the SVG as follows:
and update the SVG as follows
```xml
<svg
xmlns="http://www.w3.org/2000/svg"
class="lucide-icon"
>
<use href="#triangle-alert" />
<use
href="#alert-triangle"
/>
</svg>
<svg>
...sprite svg
</svg>
<!-- sprite SVG -->
<svg>...</svg>
```
## Icon font
### Icon Font
:::danger
[Not intended for production use.](#production-warning)
:::
Lucide icons are also available as a web font. To use them, you first need to include the corresponding stylesheet:
::: code-group
```css [Vite]
@import 'lucide-static/font/lucide.css';
```
```css [Webpack]
```css
@import ('~lucide-static/font/lucide.css');
```
```html [CDN]
<link rel="stylesheet" href="https://unpkg.com/lucide-static@latest/font/lucide.css" />
```
```html [Static asset]
<link rel="stylesheet" href="/your/path/to/lucide.css" />
```
:::
Once included, use the format `icon-{kebab-case-name}`. You can copy icon names from the [Lucide Icons page](https://lucide.dev/icons).
```html
<div class="icon-house"></div>
```
If you're not using a package manager, you can download the font files directly from the [latest release](https://github.com/lucide-icons/lucide/releases/latest).
### Node.js
## Node.js
You can also import Lucide icons in Node.js projects:
::: code-group
```js [ESM]
import {MessageSquare} from 'lucide-static';
```
```js [CommonJs]
const {MessageSquare} = require('lucide-static');
```
:::
> Note: Each icon name is in PascalCase.
#### Express app example in Node.js
To use lucide icons in your Nodejs project you can import each icon as:
```js
import express from 'express';
import {MessageSquare} from 'lucide-static';
const { messageSquare } = require('lucide-static/lib');
```
> Note: Each icon name is in camelCase.
#### Example in node.js project
```js
const express = require('express');
const { messageSquare } = require('lucide-static/lib');
const app = express();
const port = 3000;
@@ -259,7 +181,7 @@ app.get('/', (req, res) => {
</head>
<body>
<h1>Lucide Icons</h1>
<p>This is a Lucide icon ${MessageSquare}</p>
<p>This is a lucide icon ${messageSquare}</p>
</body>
</html>

View File

@@ -1,13 +1,6 @@
# Lucide Svelte
Svelte components for Lucide icons that work seamlessly with both Svelte 4 and Svelte 5. Each icon is a reactive Svelte component that renders as an inline SVG, providing excellent performance and integration with Svelte's reactive system and modern features.
**What you can accomplish:**
- Use icons as Svelte components with full reactivity and TypeScript support
- Bind icon properties to reactive variables and stores
- Create dynamic icon systems that respond to application state
- Build type-safe interfaces with comprehensive TypeScript definitions
- Optimize bundle sizes with direct icon imports and tree-shaking
Implementation of the lucide icon library for svelte applications.
## Installation
@@ -281,10 +274,10 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```svelte
<script>
import { Icon } from '@lucide/svelte';
import { pear, sausage } from '@lucide/lab';
import { burger, sausage } from '@lucide/lab';
</script>
<Icon iconNode={pear} />
<Icon iconNode={burger} />
<Icon iconNode={sausage} color="red"/>
```
@@ -303,7 +296,7 @@ The example below imports all ES Modules, so exercise caution when using it. Imp
```svelte [Svelte 5]
<script>
import * as icons from '@lucide/svelte';
let { name, ...props } = $props();
let { name } = $props();
const Icon = icons[name];
</script>

View File

@@ -1,20 +1,13 @@
# Lucide Vue Next
Vue 3 components for Lucide icons that leverage the Composition API and modern Vue features. Each icon is a reactive Vue component that renders as an inline SVG, providing excellent performance and developer experience in Vue 3 applications.
**What you can accomplish:**
- Use icons as Vue 3 components with full reactivity and TypeScript support
- Bind icon properties to reactive data and computed values
- Customize icons with props, slots, and Vue's powerful templating system
- Integrate seamlessly with Vue 3's Composition API and script setup syntax
- Build dynamic interfaces where icons respond to application state changes
Implementation of the lucide icon library for Vue 3 applications.
## Installation
::: code-group
```sh [pnpm]
pnpm add lucide-vue-next
pnpm install lucide-vue-next
```
```sh [yarn]
@@ -61,7 +54,7 @@ import { Camera } from 'lucide-vue-next';
| `size` | *number* | 24 |
| `color` | *string* | currentColor |
| `stroke-width` | *number* | 2 |
| `absoluteStrokeWidth` | *boolean* | false |
| `absolute-stroke-width` | *boolean* | false |
| `default-class` | *string* | lucide-icon |
### Applying props
@@ -88,11 +81,11 @@ This creates a single icon based on the iconNode passed and renders a Lucide ico
```vue
<script setup>
import { Icon } from 'lucide-vue-next';
import { baseball } from '@lucide/lab';
import { burger } from '@lucide/lab';
</script>
<template>
<Icon :iconNode="baseball" />
<Icon :iconNode="burger" />
</template>
```

View File

@@ -1,13 +1,6 @@
# Lucide Vue
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
- Build applications using Vue 2's familiar syntax and patterns
- Bridge the gap while planning migration to Vue 3
Implementation of the lucide icon library for Vue applications.
::: danger
This package is deprecated. Vue 2 is EOF See [Announcement](https://v2.vuejs.org/eol/). Migrate to Vue 3.
@@ -18,7 +11,7 @@ This package is deprecated. Vue 2 is EOF See [Announcement](https://v2.vuejs.or
::: code-group
```sh [pnpm]
pnpm add lucide-vue
pnpm install lucide-vue
```
```sh [yarn]
@@ -67,7 +60,7 @@ Additional props can be passed to adjust the icon:
| `size` | *number* | 24 |
| `color` | *string* | currentColor |
| `stroke-width` | *number* | 2 |
| `absoluteStrokeWidth` | *boolean* | false |
| `absolute-stroke-width` | *boolean* | false |
| `default-class` | *string* | lucide-icon |
### Applying props

View File

@@ -1,13 +1,6 @@
# Lucide
The core Lucide package for vanilla JavaScript applications. This package allows you to easily add scalable vector icons to any web project without framework dependencies. Perfect for static websites, legacy applications, or when you need lightweight icon integration with maximum browser compatibility.
**What you can accomplish:**
- Add icons to HTML using simple data attributes
- Dynamically create and insert SVG icons with JavaScript
- Customize icon appearance with CSS classes and inline styles
- Tree-shake unused icons to keep bundle sizes minimal
- Use icons in any JavaScript environment or plain HTML
Implementation of the lucide icon library for web applications.
## Installation
@@ -16,7 +9,7 @@ The core Lucide package for vanilla JavaScript applications. This package allows
::: code-group
```sh [pnpm]
pnpm add lucide
pnpm install lucide
```
```sh [yarn]
@@ -95,12 +88,7 @@ createIcons({
### Additional Options
In the `createIcons` function you can pass some extra parameters:
- you can pass `nameAttr` to adjust the attribute name to replace for
- you can pass `attrs` to pass additional custom attributes, for instance CSS classes or stroke options.
- you can pass `root` to provide a custom DOM element the icons should be replaced in (useful when manipulating small sections of a large DOM or elements in the shadow DOM)
- you can pass `inTemplates: true` to also replace icons inside `<template>` tags.
In the `createIcons` function you can pass some extra parameters to adjust the `nameAttr` or add custom attributes like for example classes.
Here is a full example:
@@ -113,9 +101,7 @@ createIcons({
'stroke-width': 1,
stroke: '#333'
},
nameAttr: 'data-lucide', // attribute for the icon name.
root: element, // DOM element to replace icons in.
inTemplates: true // Also replace icons inside <template> tags.
nameAttr: 'data-lucide' // attribute for the icon name.
});
```
@@ -133,34 +119,6 @@ createIcons({
});
```
### Custom Document root
Apply icons in a custom root element, for instance a shadow DOM root.
```js
import { createIcons } from 'lucide';
// Custom root element, for instance a shadow DOM root.
const shadowRoot = element.attachShadow({ mode: 'open' });
createIcons({
root: shadowRoot
});
```
### Apply icons inside `<template>` tags
By default icons inside `<template>` tags are not added.
By setting the `inTemplates` option to `true`, icons inside templates will also be replaced.
```js
import { createIcons } from 'lucide';
createIcons({
inTemplates: true
});
```
### Custom Element binding
```js
@@ -195,11 +153,11 @@ myApp.appendChild(menuIcon);
They can be used in the same way as the official icons.
```js
import { coconut } from '@lucide/lab';
import { burger } from '@lucide/lab';
createIcons({
icons: {
coconut
burger
}
});
```

View File

@@ -2,23 +2,18 @@ import { IconEntity } from '../../.vitepress/theme/types';
export default {
paths: async () => {
try {
const iconDetailsResponse = await fetch('https://lab.lucide.dev/api/icon-details');
const iconDetails = (await iconDetailsResponse.json()) as Record<string, IconEntity>;
const iconDetailsResponse = await fetch('https://lab.lucide.dev/api/icon-details');
const iconDetails = (await iconDetailsResponse.json()) as Record<string, IconEntity>;
return Object.values(iconDetails).map((iconEntity) => {
const params = {
externalLibrary: 'lab',
...iconEntity,
};
return Object.values(iconDetails).map((iconEntity) => {
const params = {
externalLibrary: 'lab',
...iconEntity,
};
return {
params,
};
});
} catch (error) {
console.error('Error fetching icon details:', error);
return [];
}
return {
params,
};
});
},
};

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -49,8 +49,6 @@
<path d="M120 350h240" stroke="#D8D8D9" stroke-width=".5"/>
<path d="M350 120v240" stroke="#D8D8D9" stroke-width=".5"/>
</g>
<path d="M219 396.5L208 407.5L203 402.5" stroke="#36EA83" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M241.052 414H233.06V391.455H241.118C243.386 391.455 245.338 391.906 246.975 392.809C248.611 393.704 249.87 394.992 250.751 396.673C251.639 398.353 252.083 400.364 252.083 402.705C252.083 405.054 251.639 407.072 250.751 408.76C249.87 410.448 248.604 411.743 246.953 412.646C245.309 413.549 243.342 414 241.052 414ZM237.827 409.916H240.854C242.263 409.916 243.448 409.666 244.41 409.167C245.379 408.661 246.105 407.879 246.59 406.822C247.081 405.758 247.327 404.386 247.327 402.705C247.327 401.039 247.081 399.678 246.59 398.621C246.105 397.564 245.382 396.786 244.421 396.287C243.459 395.788 242.274 395.539 240.865 395.539H237.827V409.916ZM263.166 414.33C261.456 414.33 259.977 413.967 258.729 413.24C257.489 412.507 256.531 411.486 255.856 410.18C255.181 408.866 254.843 407.344 254.843 405.612C254.843 403.865 255.181 402.338 255.856 401.032C256.531 399.718 257.489 398.698 258.729 397.972C259.977 397.238 261.456 396.871 263.166 396.871C264.876 396.871 266.351 397.238 267.591 397.972C268.839 398.698 269.8 399.718 270.475 401.032C271.151 402.338 271.488 403.865 271.488 405.612C271.488 407.344 271.151 408.866 270.475 410.18C269.8 411.486 268.839 412.507 267.591 413.24C266.351 413.967 264.876 414.33 263.166 414.33ZM263.188 410.697C263.966 410.697 264.615 410.477 265.136 410.037C265.657 409.589 266.05 408.98 266.314 408.21C266.586 407.439 266.721 406.562 266.721 405.578C266.721 404.595 266.586 403.718 266.314 402.947C266.05 402.177 265.657 401.568 265.136 401.12C264.615 400.672 263.966 400.449 263.188 400.449C262.402 400.449 261.742 400.672 261.206 401.12C260.678 401.568 260.278 402.177 260.006 402.947C259.742 403.718 259.61 404.595 259.61 405.578C259.61 406.562 259.742 407.439 260.006 408.21C260.278 408.98 260.678 409.589 261.206 410.037C261.742 410.477 262.402 410.697 263.188 410.697ZM277.279 414.286C276.552 414.286 275.928 414.029 275.407 413.516C274.893 412.995 274.637 412.371 274.637 411.644C274.637 410.925 274.893 410.308 275.407 409.795C275.928 409.281 276.552 409.024 277.279 409.024C277.983 409.024 278.6 409.281 279.128 409.795C279.656 410.308 279.921 410.925 279.921 411.644C279.921 412.129 279.796 412.573 279.546 412.976C279.304 413.373 278.985 413.692 278.589 413.934C278.192 414.169 277.756 414.286 277.279 414.286Z" fill="#36EA83"/>
<g id="embed-lucide-copy" stroke="#3C3C43" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" transform="translate(120 120), scale(10)">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -2,7 +2,6 @@ import copy from 'rollup-plugin-copy';
import replace from '@rollup/plugin-replace';
export default defineNitroConfig({
compatibilityDate: '2025-07-30',
preset: 'vercel_edge',
srcDir: '.vitepress',
routeRules: {

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