Compare commits

..
1476 changed files with 11340 additions and 20709 deletions
+1
View File
@@ -8,6 +8,7 @@ squircle
strikethrough
touchpad
ungroup
pilcrow
toc
# Brands
+4 -6
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
+2 -4
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. -->
+50 -32
View File
@@ -7,30 +7,17 @@ on:
paths:
- icons/**/*.svg
workflow_dispatch:
inputs:
version:
description: Version
required: true
permissions:
id-token: write # Required for OIDC
contents: write
# This is added to make sure we wait until last release is done, this is needed for the font-build process
concurrency:
group: continuous-integration
jobs:
create-release:
if: github.repository == 'lucide-icons/lucide' && startsWith(github.event.head_commit.message, 'feat(icons)') && github.event_name != 'workflow_dispatch'
if: github.repository == 'lucide-icons/lucide' && startsWith(github.event.head_commit.message, 'feat(icons)')
runs-on: ubuntu-latest
outputs:
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'
@@ -45,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: |
@@ -71,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
@@ -78,10 +103,3 @@ jobs:
secrets: inherit
with:
version: ${{ needs.create-release.outputs.VERSION }}
dispatch-release:
if: github.event_name == 'workflow_dispatch'
uses: './.github/workflows/release.yml'
secrets: inherit
with:
version: ${{ inputs.version }}
@@ -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})
We’re 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 }}
-1
View File
@@ -9,5 +9,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/labeler@v5
+2 -2
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'
+3 -6
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 '"
+4 -4
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'
+4 -4
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
+5 -2
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'
@@ -20,6 +20,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Outline svg Icons
run: pnpm build:outline-icons
- name: Create font in ./lucide-font
run: pnpm build:font
+2 -2
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'
+4 -4
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'
+4 -4
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'
+2 -2
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'
+4 -4
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'
+2 -2
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'
+4 -20
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'
@@ -41,19 +41,3 @@ jobs:
- name: Test
run: pnpm --filter @lucide/svelte test
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter @lucide/svelte typecheck
+4 -4
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'
+4 -4
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'
+4 -4
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'
@@ -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 }}
@@ -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 }}
+27 -25
View File
@@ -12,16 +12,15 @@ on:
description: Version
type: string
permissions:
id-token: write # Required for OIDC
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
workflow_dispatch:
inputs:
version:
description: Version
required: true
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 }}
@@ -40,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:
@@ -50,6 +48,7 @@ jobs:
'lucide',
'lucide-react',
'lucide-react-native',
'lucide-vue',
'lucide-vue-next',
'lucide-angular',
'lucide-preact',
@@ -59,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'
@@ -69,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
@@ -88,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'
@@ -103,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
@@ -122,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'
@@ -132,10 +135,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Outline svg Icons
run: pnpm build:outline-icons
- name: Create font in ./lucide-font
run: pnpm build:font --saveCodePoints
env:
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
run: pnpm build:font
- name: 'Upload to Artifacts'
uses: actions/upload-artifact@v4
@@ -147,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: |
+2 -3
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
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
+2 -6
View File
@@ -4,7 +4,6 @@
.obsidian
.now
.idea
.env
node_modules
dist
build
@@ -14,15 +13,12 @@ coverage
stats
*.log
outlined
lucide-font
packages/**/src/icons/*.js
packages/**/src/icons/*.ts
packages/**/src/icons/*.tsx
packages/**/src/aliases/*.ts
packages/**/src/aliases.ts
!packages/**/src/aliases/index.ts
packages/**/src/utils/*.ts
!packages/shared/src/utils/*.ts
packages/**/src/dynamicIconImports.ts
packages/**/DynamicIcon.d.ts
packages/**/dynamicIconImports.js
@@ -43,11 +39,11 @@ docs/.vitepress/.temp
docs/.vitepress/data/iconNodes
docs/.vitepress/data/iconMetaData.ts
docs/.vitepress/data/releaseMetaData.json
docs/.vitepress/data/releaseMetadata
docs/.vitepress/data/releaseMetaData
docs/.vitepress/data/categoriesData.json
docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vitepress/data/brandStopwords.json
docs/.vercel
docs/.nitro
.gitignore
+1
View File
@@ -0,0 +1 @@
auto-install-peers=true
-11
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`.",
-71
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.
+3 -25
View File
@@ -25,7 +25,7 @@ Guidelines for pull requests:
Please make sure you follow the icon guidelines, that should be followed to keep quality and consistency when making icons for Lucide.
Read it here: [ICON_GUIDELINES](https://lucide.dev/contribute/icon-design-guide).
Read it here: [ICON_GUIDELINES](https://lucide.dev/docs/icon-design-guide).
#### Lucide Studio
@@ -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.
+1 -25
View File
@@ -1,6 +1,6 @@
ISC License
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2026 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2026.
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-2026 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.
+2 -17
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!
@@ -88,16 +82,7 @@ Thank you to all the people who contributed to Lucide!
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="docs/public/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
[//]: <> (Open Collective backers)
### Awesome backers 🍺
<a href="https://github.com/pdfme/pdfme"><img src="docs/public/sponsors/pdfme.svg" width="180" alt="pdfme – Open-source PDF generation library built with TypeScript and React." /></a>
<a href="https://www.paxhistoria.co/"><img src="docs/public/sponsors/paxhistoria.svg?" width="180" alt="Pax Historia – An alternate history sandbox game" /></a>
### Backers ☕
<a href="https://www.fina.money/"><img src="docs/public/sponsors/fina-money.png" width="180" alt="Fina Money – Modular Finance Tracker" /></a>
### Other contributors 💸
You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons).
<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>
-150
View File
@@ -1,150 +0,0 @@
{
"adobe": "Adobe",
"airplay": "AirPlay",
"amazon": "Amazon",
"android": "Android",
"angular": "Angular",
"aws": "AWS",
"azure": "Azure",
"bandcamp": "Bandcamp",
"behance": "Behance",
"bitbucket": "Bitbucket",
"blender": "Blender",
"bluesky": "BlueSky",
"bootstrap": "Bootstrap",
"brave": "Brave",
"chakra": "Chakra UI",
"chrome": "Chrome",
"codepen": "Codepen",
"codesandbox": "CodeSandbox",
"csharp": "C#",
"cypress": "Cypress",
"dart": "Dart",
"deezer": "Deezer",
"deno": "Deno",
"discord": "Discord",
"docker": "Docker",
"dribbble": "Dribbble",
"dropbox": "Dropbox",
"edge": "Edge",
"ember": "Ember",
"epic": "Epic Games",
"erlang": "Erlang",
"esbuild": "esbuild",
"eslint": "ESLint",
"facebook": "Facebook",
"figjam": "FigJam",
"figma": "Figma",
"firebase": "Firebase",
"firefox": "Firefox",
"framer": "Framer",
"gatsby": "Gatsby",
"gcp": "Google Cloud",
"github": "GitHub",
"gitlab": "GitLab",
"golang": "GoLang",
"google": "Google",
"gmail": "Gmail",
"gravatar": "Gravatar",
"haskell": "Haskell",
"instagram": "Instagram",
"java": "Java",
"javascript": "JavaScript",
"jest": "Jest",
"jira": "Jira",
"kotlin": "Kotlin",
"kubernetes": "Kubernetes",
"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"
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Navigation, Maps, and POIs",
"title": "Navigation",
"icon": "compass"
}
@@ -1,4 +1,3 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../data/iconMetaData';
export default eventHandler((event) => {
-1
View File
@@ -1,4 +1,3 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconNodes from '../../data/iconNodes/index.ts';
import { IconNodeWithKeys } from '../../theme/types';
import iconMetaData from '../../data/iconMetaData';
@@ -1,6 +1,6 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import React from 'react';
import { createElement } from 'react';
import SvgPreview from '../../lib/SvgPreview/index.tsx';
import iconNodes from '../../data/iconNodes';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
@@ -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 = [];
@@ -34,29 +32,25 @@ export default eventHandler((event) => {
const iconNode = iconNodes[backdropName];
const LucideIcon = createLucideIcon(backdropName, iconNode);
const svg = renderToStaticMarkup(<LucideIcon />);
const svg = renderToStaticMarkup(createElement(LucideIcon));
const backdropString = svg.replaceAll('\n', '').replace(/<svg[^>]*>|<\/svg>/g, '');
children.push(
<Backdrop
backdropString={backdropString}
src={src.replace(/<svg[^>]*>|<\/svg>/g, '')}
color="#777"
/>,
createElement(Backdrop, {
backdropString,
src,
color: '#777',
}),
);
}
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(createElement(SvgPreview, { src, showGrid: true }, children)),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return renderToString(
<SvgPreview
src={src.replace(/<svg[^>]*>|<\/svg>/g, '')}
showGrid
height={height}
width={width}
>
{children}
</SvgPreview>,
);
return svg;
});
@@ -1,6 +1,6 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import React from 'react';
import { createElement } from 'react';
import Diff from '../../../lib/SvgPreview/Diff.tsx';
import iconNodes from '../../../data/iconNodes';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
@@ -17,35 +17,21 @@ 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 Icon = createLucideIcon(name, iconNodes[name]);
const oldSrc = iconNodes[name]
? renderToStaticMarkup(<Icon />)
? renderToStaticMarkup(createElement(createLucideIcon(name, iconNodes[name])))
.replaceAll('\n', '')
.replace(/<svg[^>]*>|<\/svg>/g, '')
: '';
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(createElement(Diff, { oldSrc, newSrc, showGrid: true }, children)),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return renderToString(
<Diff
oldSrc={oldSrc}
newSrc={newSrc}
showGrid
height={height}
width={width}
>
{children}
</Diff>,
);
return svg;
});
@@ -1,52 +1,45 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import iconNodes from '../../../data/iconNodes';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import createLucideIcon from 'lucide-react/src/createLucideIcon';
import sharp from 'sharp';
import { Resvg, initWasm } from '@resvg/resvg-wasm';
import wasm from './loadWasm';
var initializedResvg = initWasm(wasm);
export default eventHandler(async (event) => {
const { params = {} } = event.context;
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 pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer();
const resvg = new Resvg(svg, { background: '#000' });
const pngData = resvg.render();
const pngBuffer = Buffer.from(pngData.asPng());
defaultContentType(event, 'image/svg+xml');
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; }
@@ -59,20 +52,20 @@ export default eventHandler(async (event) => {
<mask id="mask">
<image
width="${imageSize}"
height="${prevSvg ? imageSize * 2 : imageSize}"
href="data:image/png;base64,${pngBuffer.toString('base64')}"
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)"
/>
@@ -0,0 +1,15 @@
import fs from 'fs';
import module from 'node:module';
/* WASM_IMPORT */
let wasm;
if (process.env.NODE_ENV === 'development') {
const require = module.createRequire(import.meta.url);
wasm = fs.readFileSync(require.resolve('@resvg/resvg-wasm/index_bg.wasm'));
} else {
wasm = resvg_wasm;
}
export default wasm;
@@ -0,0 +1,44 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString } from 'react-dom/server';
import { createElement } from 'react';
import SvgPreview from '../../../lib/SvgPreview/index.tsx';
import createLucideIcon, { IconNode } from 'lucide-react/src/createLucideIcon';
import { parseSync } from 'svgson';
export default eventHandler((event) => {
const { params } = event.context;
const [strokeWidth, svgData] = params.data.split('/');
const data = svgData.slice(0, -4);
const src = Buffer.from(data, 'base64').toString('utf8');
const Icon = createLucideIcon(
'icon',
parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`).children.map(
({ name, attributes }) => [name, attributes],
) as IconNode,
);
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(createElement(Icon, { strokeWidth }))
.replace(/fill\="none"/, 'fill="#fff"')
.replace(
/>/,
`><style>
@media screen and (prefers-color-scheme: light) {
svg { fill: transparent !important; }
}
@media screen and (prefers-color-scheme: dark) {
svg { stroke: #fff; fill: transparent !important; }
}
</style>`,
),
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return svg;
});
@@ -1,37 +0,0 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString } from 'react-dom/server';
import React from 'react';
import Icon from 'lucide-react/src/Icon';
import type { IconNode } from 'lucide-react/src/types';
import { parseSync } from 'svgson';
export default eventHandler((event) => {
const { params } = event.context;
const [strokeWidth, svgData] = params.data.split('/');
const data = svgData.slice(0, -4);
const src = Buffer.from(data, 'base64').toString('utf8');
const iconNode = parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`).children.map(
({ name, attributes }) => [name, attributes],
) as IconNode;
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return renderToString(
<Icon
iconNode={iconNode}
strokeWidth={strokeWidth}
>
<style>
{`@media screen and (prefers-color-scheme: light) {
svg { fill: transparent !important; }
}
@media screen and (prefers-color-scheme: dark) {
svg { stroke: #fff; fill: transparent !important; }
}`}
</style>
</Icon>,
);
});
@@ -1,58 +0,0 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString } from 'react-dom/server';
import React from 'react';
import Diff from '../../../lib/SvgPreview/Diff.tsx';
export default eventHandler((event) => {
const { params } = event.context;
const pathData = params.data.split('/');
const data = pathData.at(-1).slice(0, -4);
const [operation] = pathData;
const newSrc = Buffer.from(data, 'base64')
.toString('utf8')
.replaceAll('\n', '')
.replace(/<svg[^>]*>|<\/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 '';
}
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
return renderToString(
<Diff
oldSrc={oldSrc}
newSrc={newSrc}
showGrid
height={height}
width={width}
>
{children}
</Diff>,
);
});
@@ -22,7 +22,6 @@ export default eventHandler((event) => {
const height = getQuery(event).height || undefined;
const color = getQuery(event).color || undefined;
const strokeWidth = getQuery(event).strokeWidth || undefined;
const background = getQuery(event).background || undefined;
const LucideIcon = createLucideIcon(params.iconName, iconNode);
@@ -33,7 +32,6 @@ export default eventHandler((event) => {
height,
color: color ? `#${color}` : undefined,
strokeWidth,
style: background ? { background } : undefined,
}),
),
).toString('utf8');
-1
View File
@@ -1,4 +1,3 @@
import { eventHandler, setResponseHeader } from 'h3';
import iconMetaData from '../../data/iconMetaData';
export default eventHandler((event) => {
+3
View File
@@ -0,0 +1,3 @@
export default eventHandler(() => {
return { nitro: 'Is Awesome! asda' };
});
+1 -38
View File
@@ -1,7 +1,6 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vitepress';
import sidebar from './sidebar';
import getStructuredData from './getStructuredData';
const title = 'Lucide';
const socialTitle = 'Lucide Icons';
@@ -10,7 +9,6 @@ const description = 'Beautiful & consistent icon toolkit made by the community.'
// https://vitepress.dev/reference/site-config
export default defineConfig({
title,
titleTemplate: ':title \u2013 Lucide',
description,
cleanUrls: true,
outDir: '.vercel/output/static',
@@ -38,13 +36,6 @@ export default defineConfig({
},
},
head: [
[
'link',
{
rel: 'preconnect',
href: 'https://analytics.lucide.dev',
},
],
[
'script',
{
@@ -152,36 +143,8 @@ export default defineConfig({
},
],
],
async transformPageData(pageData) {
if (
pageData.relativePath.startsWith('icons/') &&
!pageData.relativePath.startsWith('icons/lab/') &&
pageData.params?.name
) {
const iconName = pageData.params.name;
pageData.title = `${iconName} icon details`;
const taggedAs = pageData.params?.tags?.length
? `Tagged as: ${pageData.params.tags.join(', ')}.`
: '';
const categorizedIn = pageData.params?.category?.length
? `Categorized in: ${pageData.params.category.join(', ')}.`
: '';
pageData.description =
`Details and related icons for ${iconName} icon. ${taggedAs} ${categorizedIn}`.trim();
const structuredData = await getStructuredData(iconName, pageData);
pageData.frontmatter.head ??= [];
pageData.frontmatter.head.push([
'script',
{ type: 'application/ld+json' },
JSON.stringify(structuredData),
]);
}
},
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
logo: {
light: '/logo.light.svg',
dark: '/logo.dark.svg',
+1 -1
View File
@@ -101,7 +101,7 @@
},
{
"name": "navigation",
"title": "Navigation, Maps, and POIs"
"title": "Navigation"
},
{
"name": "notifications",
-16
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",
@@ -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,119 +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"
},
{
"name": "lucide-web-components",
"description": "Implementation of Lucide icons in Web Components custom elements.",
"icon": "/framework-logos/web-components.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/npm/v/@dile/iconlib",
"href": "https://dile-components.com/icons/lucide-icons/"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/npm/dt/@dile/iconlib",
"href": "https://dile-components.com/icons/lucide-icons/"
}
],
"source": "https://github.com/Polydile/dile-components",
"documentation": "https://github.com/Polydile/dile-components/blob/master/packages/iconlib/README.md"
},
{
"name": "strapi-lucide-icons",
"description": "Lucide icon picker custom field for Strapi.",
"icon": "/framework-logos/strapi.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/npm/v/strapi-lucide-icons",
"href": "https://www.npmjs.com/package/strapi-lucide-icons"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/npm/dw/strapi-lucide-icons",
"href": "https://www.npmjs.com/package/strapi-lucide-icons"
}
],
"source": "https://github.com/shx08/strapi-lucide-icons",
"documentation": "https://github.com/shx08/strapi-lucide-icons/blob/main/README.md"
}
]
-46
View File
@@ -1,46 +0,0 @@
import { PageData } from 'vitepress';
export default async function getStructuredData(iconName: string, pageData: PageData) {
const url = `https://lucide.dev/icons/${iconName}`;
return {
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': url,
name: pageData.title,
description: pageData.description,
url,
inLanguage: 'en',
isPartOf: {
'@type': 'WebSite',
name: 'Lucide Icons',
url: 'https://lucide.dev',
},
breadcrumb: {
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Icons', item: 'https://lucide.dev/icons' },
{ '@type': 'ListItem', position: 2, name: iconName, item: url },
],
},
mainEntity: {
'@type': 'ImageObject',
'@id': `${url}#icon`,
name: iconName,
// TODO: Add support for description extraction from icon metadata
// description: 'An ...SVG icon from the Lucide icon set.',
contentUrl: `https://lucide.dev/api/icons/${iconName}}?width=24&height=24&background=white`,
thumbnailUrl: `https://lucide.dev/api/icons/${iconName}}?width=256&height=256&background=white`,
encodingFormat: 'image/svg+xml',
keywords: pageData.params?.tags,
width: 24,
height: 24,
creator: {
'@type': 'Organization',
name: 'Lucide Icons',
},
datePublished: pageData?.params?.createdRelease?.date,
dateModified: pageData?.params?.changedRelease?.date,
},
};
}
+5 -5
View File
@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
interface BackdropProps {
src: string;
@@ -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"
+6 -10
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"
/>
@@ -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));
}
+98 -166
View File
@@ -1,7 +1,6 @@
import React, { Fragment } from 'react';
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,63 +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>
);
};
interface SvgPreviewProps extends React.SVGProps<SVGSVGElement> {
height?: number;
width?: number;
src: string | ReturnType<typeof getPaths>;
showGrid?: boolean;
children?: React.ReactNode;
}
const SvgPreview = (
{ src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps,
ref,
) => {
const subGridSize =
Math.max(height, width) % 3 === 0
? Math.max(height, width) > 24
? 12
: 3
: Math.max(height, width) % 5 === 0
? 5
: 0;
const SvgPreview = React.forwardRef<
SVGSVGElement,
{
src: string | ReturnType<typeof getPaths>;
showGrid?: boolean;
} & React.SVGProps<SVGSVGElement>
>(({ 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}
@@ -428,12 +372,8 @@ const SvgPreview = (
<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}
/>
@@ -445,12 +385,6 @@ const SvgPreview = (
radius={1}
strokeOpacity={0.15}
/>
<GapViolationHighlight
paths={paths}
stroke="red"
strokeOpacity={0.75}
strokeWidth={4}
/>
<Handles
paths={paths}
strokeWidth={0.12}
@@ -497,8 +431,6 @@ const SvgPreview = (
{children}
</svg>
);
};
SvgPreview.displayName = 'SvgPreview';
});
export default SvgPreview;
@@ -1,19 +0,0 @@
import { memoize } from 'lodash-es';
import SVGPathCommander from 'svg-path-commander';
import { Path } from './types';
function pathToPoints({ d, prev, next }: Path, interval = 1) {
const commander = new SVGPathCommander(d);
const points = [];
try {
const totalLength = commander.getTotalLength();
points.push(prev);
for (let i = interval; i < totalLength - interval; i += interval) {
points.push(commander.getPointAtLength(i));
}
points.push(next);
} catch (err) {}
return points;
}
export default memoize(pathToPoints);
+1 -1
View File
@@ -8,7 +8,7 @@ export type Path = {
prev: Point;
next: Point;
isStart: boolean;
circle?: { x: number; y: number; r: number; tangentIntersection?: Point };
circle?: { x: number; y: number; r: number };
cp1?: Point;
cp2?: Point;
c: ReturnType<typeof getCommands>[number];
+6 -59
View File
@@ -2,7 +2,6 @@ import { INode, parseSync } from 'svgson';
import toPath from 'element-to-path';
import { SVGPathData, encodeSVGPath } from 'svg-pathdata';
import { Path, Point } from './types';
import { memoize } from 'lodash-es';
function assertNever(x: never): never {
throw new Error('Unknown type: ' + x['type']);
@@ -45,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;
@@ -242,7 +237,6 @@ const getPaths = (src: string) => {
break;
}
default: {
// @ts-ignore
assertNever(c);
}
}
@@ -250,7 +244,7 @@ const getPaths = (src: string) => {
return paths;
};
const arcEllipseCenter = (
export const arcEllipseCenter = (
x1: number,
y1: number,
rx: number,
@@ -302,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);
@@ -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,22 +102,27 @@ 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>
`,
},
];
};
const highlighter = await createHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});
export type ThemeOptions =
| ThemeRegistration
| { light: ThemeRegistration; dark: ThemeRegistration };
export const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});
const highlightedCode = highlighter
.codeToHtml(code, {
lang,
@@ -1,4 +1,5 @@
import { highLightCode } from './createCodeExamples';
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
type CodeExampleType = {
title: string;
@@ -9,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({
@@ -21,9 +21,8 @@ createIcons({
$CamelCase
}
});
</script>
<i data-lucide="$Name"></i>\
document.body.append('<i data-lucide="$Name"></i>');\
`,
},
{
@@ -104,17 +103,45 @@ import { $CamelCase } from '@lucide/lab';
@NgModule({
imports: [
LucideAngularModule.pick({ $PascalCase: $CamelCase })
LucideAngularModule.pick({ $CamelCase })
],
})
// app.component.html
<lucide-icon name="$PascalCase"></lucide-icon>
<lucide-icon name="$CamelCase"></lucide-icon>
`,
},
];
};
export type ThemeOptions =
| ThemeRegistration
| { light: ThemeRegistration; dark: ThemeRegistration };
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});
const highlightedCode = highlighter
.codeToHtml(code, {
lang,
themes: {
light: 'github-light',
dark: 'github-dark',
},
defaultColor: false,
})
.replace('shiki-themes', 'shiki-themes vp-code');
return `<div class="language-${lang} ${active ? 'active' : ''}">
<button title="Copy Code" class="copy"></button>
<span class="lang">${lang}</span>
${highlightedCode}
</div>`;
};
export default async function createCodeExamples() {
const codes = getIconCodes();
@@ -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),
});
+1 -1
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';
@@ -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,33 +45,27 @@ 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: 13px;
font-size: 14px;
text-align: left;
border: 1px solid transparent;
cursor: text;
display: flex;
align-items: center;
gap: 2px;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
}
.color-input-text {
@@ -90,24 +75,19 @@ const value = computed({
border: none;
background: transparent;
color: var(--vp-c-text-1);
font-size: 13px;
font-size: 14px;
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-within {
.color-picker:hover, .color-picker:focus {
border-color: var(--vp-c-brand);
}
.color-picker:focus-within {
background-color: var(--vp-c-bg);
background: var(--vp-c-bg-alt);
}
.color-input[value='currentColor'] {
.color-input[value="currentColor"] {
}
</style>
@@ -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,33 +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);
}
.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;
}
background: var(--vp-c-bg-alt);
}
</style>
@@ -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 {
+17 -132
View File
@@ -1,93 +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`);
}
});
};
const isMac = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
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"
:style="{ right: isMac ? '50px' : '68px' }"
>
<Icon
:iconNode="x"
:size="20"
/>
</IconButton>
<kbd
v-if="shortcut"
class="shortcut"
ref="shortcutEl"
>{{ shortcut }}</kbd
>
</div>
</template>
@@ -95,7 +46,6 @@ defineExpose({
.input-wrapper {
position: relative;
}
.input {
justify-content: flex-start;
border: 1px solid transparent;
@@ -105,84 +55,22 @@ 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);
}
.input:focus {
background-color: var(--vp-c-bg);
background: var(--vp-c-bg-alt);
}
.input.has-icon {
padding-left: 52px;
}
.clear-button {
position: absolute;
right: 68px;
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;
}
.input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
display: none;
}
.input[type="search"]::-ms-clear {
display: none;
}
.input[type="search"]::-o-clear {
display: none;
}
.input[type="search"]::-moz-clear {
display: none;
}
@media (hover: none) {
.shortcut {
display: none;
}
.clear-button {
right: 16px !important;
}
}
</style>
<style>
.input-wrapper > svg {
.input-wrapper svg {
position: absolute;
left: 16px;
top: 12px;
@@ -190,7 +78,4 @@ defineExpose({
color: var(--vp-c-text-2);
pointer-events: none;
}
.input-wrapper > svg:has(+ .input.has-icon:focus) {
color: var(--vp-c-text-1);
}
</style>
@@ -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>
@@ -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);
}
@@ -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>
@@ -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>
@@ -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"
@@ -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',
},
],
};
},
@@ -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) {
@@ -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>
@@ -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>
@@ -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<{
@@ -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 {
@@ -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">
@@ -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…`"
: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>
@@ -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…`"
: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>
@@ -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 won’t 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 didn’t 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 isn’t.',
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: 'It’s 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: 'It’s Dangerous to Go Alone',
message: 'Take this icon instead — it’s not [name], but it might help.',
icon: markRaw(sword),
},
]);
const notFoundPlaceholders: Omit<Placeholder, 'title'>[] = shallowReadonly([
{
message: 'We’ve looked for this icon for a bird’s eye view, but could not find it.',
icon: markRaw(bird),
},
{
message: 'We checked every tree. Only acorns, no results.',
icon: markRaw(squirrel),
},
{
message: 'You’ve gone too deep into the rabbit hole — this icon doesn’t 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 hasn’t 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 doesn’t 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>
@@ -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>
@@ -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>;
+40 -2
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 {
@@ -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;
}
@@ -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 };
}
@@ -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;
}
-2
View File
@@ -1,7 +1,5 @@
---
aside: false
title: Contributing to Lucide
description: Guidelines and instructions for contributing to the Lucide project.
---
<!--@include: ../CONTRIBUTING.md -->
+4 -11
View File
@@ -10,11 +10,6 @@ because they can quickly give information.
However, not everyone can understand them easily. When using icons it is very important to consider
the following aspects of accessibility.
::: info
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can make them accessible yourself by following the guidelines below.
:::
## Provide visible labels
Icons are a helpful tool to improve perception, but they aren't a replacement for text.
@@ -144,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>
```
@@ -172,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).
@@ -185,7 +179,6 @@ import { AccessibleIcon } from '@radix-ui/react-accessible-icon';
<ArrowRightIcon />
</AccessibleIcon>
```
### Example - Bootstrap
```html
+3 -3
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";
```
+1 -14
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`.
@@ -90,15 +86,6 @@ Here are rules that should be followed to keep quality and consistency when maki
**Tip:** whenever possible align elements and arc centers to the grid.
### 14. Icons should share common shapes
You should try to create consistent groups and variants, reuse and try to create uniformity.
Consistency inside groups and variants has a lower priority than the rules above.
**Example:** All `-off` icons should look the same unless it for example violates the optical volume rule.
**Tip:** Try to not move the base shape to enable better use in a toggle context.
## Naming conventions
1. Icon names use lower kebab case.\
+1 -1
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
+12 -13
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,23 +91,22 @@ Implementation of the lucide icon library for Svelte applications.
::: code-group
```sh [pnpm]
pnpm add @lucide/svelte
pnpm install lucide-svelte
```
```sh [yarn]
yarn add @lucide/svelte
yarn add lucide-svelte
```
```sh [npm]
npm install @lucide/svelte
npm install lucide-svelte
```
```sh [bun]
bun add @lucide/svelte
bun add lucide-svelte
```
:::
> `@lucide/svelte` is only for Svelte 5, for Svelte 4 use the `lucide-svelte` package.
For more details, see the [documentation](packages/lucide-svelte.md).
@@ -118,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]
@@ -144,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]
@@ -170,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]
@@ -197,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]
@@ -223,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]
+8 -27
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,24 +151,12 @@ 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 { }
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```html
<lucide-icon name="check" aria-label="Task completed"></lucide-icon>
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
+3 -26
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',
@@ -192,19 +185,3 @@ import LucideIcon from './LucideIcon.astro';
<LucideIcon name="Menu" />
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
---
import { Check } from '@lucide/astro';
---
<Check aria-label="Task completed" />
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
+5 -28
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,24 +119,8 @@ export default Icon;
import Icon from './Icon';
const App = () => {
return <Icon name="house" />;
return <Icon name="home" />;
};
export default App;
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
import { Check } from 'lucide-preact';
const App = () => {
return <Check aria-label="Task completed" />;
};
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
+9 -22
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} />
);
```
@@ -102,16 +95,10 @@ The example below imports all ES Modules, so exercise caution when using it. Imp
### Icon Component Example
```tsx
import * as icons from 'lucide-react-native/icons';
```jsx
import { icons } from 'lucide-react-native';
interface IconProps {
name: keyof typeof icons;
color?: string;
size?: number;
}
const Icon = ({ name, color, size }: IconProps) => {
const Icon = ({ name, color, size }) => {
const LucideIcon = icons[name];
return <LucideIcon color={color} size={size} />;
@@ -122,11 +109,11 @@ export default Icon;
#### Using the Icon Component
```tsx
```jsx
import Icon from './Icon';
const App = () => {
return <Icon name="House" />;
return <Icon name="home" />;
};
export default App;
+6 -28
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.
@@ -109,19 +103,3 @@ const App = () => (
<DynamicIcon name="camera" color="red" size={48} />
);
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
import { Check } from 'lucide-react';
const App = () => {
return <Check aria-label="Task completed" />;
};
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).
+3 -26
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"/>
@@ -144,19 +137,3 @@ const App = () => {
export default App;
```
## Accessibility
By default, we hide icons from screen readers using `aria-hidden="true"`.
You can add accessibility attributes using aria-labels.
```jsx
import { Check } from 'lucide-solid';
const App = () => {
return <Check aria-label="Task completed" />;
};
```
For best practices on accessibility, please see our [accessibility guide](../advanced/accessibility.md).

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