Compare commits

..

19 Commits

Author SHA1 Message Date
Eric Fennis
248d1c31eb Adds release config 2025-01-03 15:50:42 +01:00
Eric Fennis
b6a978ec6a Use latest version of semantic version 2025-01-03 15:39:45 +01:00
Eric Fennis
de454e4d98 Add version 2025-01-03 15:38:15 +01:00
Eric Fennis
13c4163843 Add permissions 2025-01-03 15:37:53 +01:00
Eric Fennis
c0d4147fe6 fix: Test Version 2025-01-03 15:33:47 +01:00
Eric Fennis
0640cd0938 Test workflow 2025-01-03 15:27:19 +01:00
Eric Fennis
7ab9b83991 fix: release workflow 2025-01-03 15:22:20 +01:00
Eric Fennis
0c08347e5e Test workflpw 2025-01-03 15:01:08 +01:00
Eric Fennis
35e9cb4b6f Test workflow 2025-01-03 15:00:13 +01:00
Eric Fennis
2095ba6aa7 ci: Add commit message check 2024-10-10 05:47:54 +02:00
Eric Fennis
ddfc5cadc1 Merge branch 'main' of https://github.com/lucide-icons/lucide into new-release-workflow 2024-10-10 05:36:21 +02:00
Eric Fennis
f3c6929c98 Remove Node version 2024-10-10 05:36:17 +02:00
Eric Fennis
9b46e0dcba Add name in package.json 2024-10-10 05:34:06 +02:00
Eric Fennis
ef449fd5c5 Try mono repo setup 2024-10-10 05:29:12 +02:00
Eric Fennis
2892be8229 Add temporary branch test 2024-10-10 05:21:06 +02:00
Eric Fennis
2b1ad320fe Add PR title linting 2024-10-10 05:16:17 +02:00
Eric Fennis
61a86a959a enable dry run 2024-10-08 11:44:49 +02:00
Eric Fennis
d92e7796e4 test worflow 2024-10-08 11:43:44 +02:00
Eric Fennis
d237803413 Adds semantic release step in CI action 2024-07-22 11:37:06 +02:00
1835 changed files with 19337 additions and 37104 deletions

View File

@@ -8,10 +8,10 @@ squircle
strikethrough strikethrough
touchpad touchpad
ungroup ungroup
pilcrow
toc toc
# Brands # Brands
codepen codepen
codesandbox codesandbox
dribbble dribbble
x.com

View File

@@ -7,10 +7,8 @@ body:
value: | value: |
Before submitting an icon request check if it has already been requested. If there is an open request, please add a 👍. Before submitting an icon request check if it has already been requested. If there is an open request, please add a 👍.
> [!CAUTION] **Important note**: No new brand logos are allowed, see https://github.com/lucide-icons/lucide/issues/670.
> 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 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.
>
> Existing brand icons are being phased out. Consider using https://simpleicons.org, which offers purpose-built SVGs that are also on a 24×24px grid.
- type: input - type: input
id: name id: name
attributes: attributes:
@@ -43,9 +41,9 @@ body:
required: true required: true
- label: I have searched existing icons to make sure it does not already exist and I didn't find any. - label: I have searched existing icons to make sure it does not already exist and I didn't find any.
required: true 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 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 required: true
- label: I have provided appropriate use cases for the icon(s) requested. - label: I have provided appropriate use cases for the icon(s) requested.
required: true required: true

View File

@@ -22,7 +22,6 @@ body:
- label: lucide-svelte - label: lucide-svelte
- label: lucide-vue - label: lucide-vue
- label: lucide-vue-next - label: lucide-vue-next
- label: lucide-astro
- label: Figma plugin - label: Figma plugin
- label: source/main - label: source/main
- label: other/not relevant - label: other/not relevant

View File

@@ -22,7 +22,6 @@ body:
- label: lucide-svelte - label: lucide-svelte
- label: lucide-vue - label: lucide-vue
- label: lucide-vue-next - label: lucide-vue-next
- label: lucide-astro
- label: Figma plugin - label: Figma plugin
- label: all JS packages - label: all JS packages
- label: site - label: site

44
.github/actions/build-and-test.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: 'Build and Test'
description: 'Builds and test a package'
inputs:
name:
description: “Name of the package”
required: true
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --filter lucide-preact
- name: Build
run: pnpm --filter lucide-preact build
- name: Test
run: pnpm --filter lucide-preact test

41
.github/actions/check-icons.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: 'Check icons'
description: 'Cross-checks icon and category references in JSON descriptors'
inputs:
name:
description: “Name of the package”
required: true
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
id: pnpm-install
with:
version: 8
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --filter .
- name: Check icons and categories
run: pnpm checkIcons

6
.github/labeler.yml vendored
View File

@@ -79,12 +79,6 @@
- any-glob-to-any-file: - any-glob-to-any-file:
- 'packages/lucide-solid/*' - 'packages/lucide-solid/*'
# For changes in the lucide astro package
🚀 astro package:
- changed-files:
- any-glob-to-any-file:
- 'packages/astro/*'
# For changes in the lucide static package # For changes in the lucide static package
🪨 static package: 🪨 static package:
- changed-files: - changed-files:

View File

@@ -1,18 +1,16 @@
<!-- Thank you for contributing! --> <!-- Thank you for contributing! -->
<!--
PR Title Guidelines:
Please use the format: <type>(<scope>): <short description>
Example: feat(icons): added `camera` icon
Available types: fix, feat, perf, docs, style, refactor, test, chore, ci, build
Common scopes: icons, docs, studio, site, dev
-->
<!-- Insert `closes #issueNumber` here if merging this PR will resolve an existing issue --> <!-- Insert `closes #issueNumber` here if merging this PR will resolve an existing issue -->
## Description
## What is the purpose of this pull request?
<!-- Please choose one of the following, and put an "x" next to it. -->
- [ ] New Icon
- [ ] Bug fix
- [ ] New Feature
- [ ] Documentation update
- [ ] Other:
### Description
<!-- Please insert your description here and provide info about the "what" this PR is contribution --> <!-- Please insert your description here and provide info about the "what" this PR is contribution -->
### Icon use case <!-- ONLY for new icons, remove this part if not icon PR --> ### Icon use case <!-- ONLY for new icons, remove this part if not icon PR -->
@@ -25,12 +23,10 @@ Common scopes: icons, docs, studio, site, dev
### Concept <!-- ONLY for new icons --> ### Concept <!-- ONLY for new icons -->
<!-- All of these requirements must be fulfilled. --> <!-- 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 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 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. --> ### Author, credits & license<!-- ONLY for new icons. -->
<!-- Please choose one of the following, and put an "x" next to it. --> <!-- Please choose one of the following, and put an "x" next to it. -->

View File

@@ -3,69 +3,67 @@ name: Continuous integration icons
on: on:
push: push:
branches: branches:
- main # - main
paths: - '*'
- icons/**/*.svg # paths:
# - icons/**/*.svg
permissions:
id-token: write # Required for OIDC
contents: write
jobs: jobs:
create-release: create-release:
if: github.repository == 'lucide-icons/lucide' && startsWith(github.event.head_commit.message, 'feat(icons)') # Only create a new releases for new icons
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
packages: read
actions: write
issues: write
pull-requests: write
outputs: outputs:
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }} VERSION: ${{ steps.semantic.outputs.new_release_version }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6
- name: Semantic Release
id: semantic
uses: cycjimmy/semantic-release-action@v4
with: with:
cache: 'pnpm' tag_format: ${version}
node-version-file: 'package.json' branches: |
['new-release-workflow']
extends: |
semantic-release-monorepo
extra_plugins: |
@semantic-release/github
@semantic-release/git
@semantic-release/release-notes-generator
conventional-changelog-conventionalcommits
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Fetch tags
run: git fetch --all --tags
- name: Get latest tag
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: env:
LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create new version - name: Do something when a new release published
id: new-version if: steps.semantic.outputs.new_release_published == 'true'
run: echo "NEW_VERSION=$(pnpm semver $LATEST_TAG -i minor)" >> $GITHUB_OUTPUT
env:
LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }}
- name: Check output
run: | run: |
echo '${{ steps.new-version.outputs.NEW_VERSION }}' echo ${{ steps.semantic.outputs.new_release_version }}
echo '${{ steps.change-log.outputs.CHANGE_LOG }}' echo ${{ steps.semantic.outputs.new_release_major_version }}
echo ${{ steps.semantic.outputs.new_release_minor_version }}
echo ${{ steps.semantic.outputs.new_release_patch_version }}
# - name: Create Release
# uses: softprops/action-gh-release@v1
# with:
# tag_name: ${{ steps.semantic.outputs.new_release_version }}
# name: Version ${{ steps.semantic.outputs.new_release_version }}
# body: ${{ steps.semantic.outputs.new_release_notes }}
- name: Create Release # start-release:
uses: softprops/action-gh-release@v1 # if: github.repository == 'lucide-icons/lucide'
with: # needs: create-release
tag_name: ${{ steps.new-version.outputs.NEW_VERSION }} # uses: './.github/workflows/release.yml'
name: Version ${{ steps.new-version.outputs.NEW_VERSION }} # secrets: inherit
generate_release_notes: true # with:
# version: ${{ needs.create-release.outputs.VERSION }}
start-release:
if: github.repository == 'lucide-icons/lucide'
needs: create-release
uses: './.github/workflows/release.yml'
secrets: inherit
with:
version: ${{ needs.create-release.outputs.VERSION }}

View File

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

View File

@@ -1,58 +0,0 @@
name: Icon preview comment
on:
workflow_run:
workflows: ['Pull request icon preview']
types:
- completed
jobs:
upload:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Download artifact'
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr_number"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
const fs = require('fs');
fs.writeFileSync('${{github.workspace}}/pr_number.zip', Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip pr_number.zip
- name: 'Get PR number'
run: echo "number=$(cat NR)" >> $GITHUB_OUTPUT
id: pr-number
- name: Find Comment
uses: peter-evans/find-comment@v2
id: pr-comment
with:
issue-number: ${{ steps.pr-number.outputs.number }}
comment-author: 'github-actions[bot]'
body-includes: Added or changed icons
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.pr-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-number.outputs.number }}
body-path: comment-markup.md
edit-mode: replace

View File

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

View File

@@ -1,23 +0,0 @@
name: Linting PR
on:
pull_request:
paths-ignore:
- icons/*.svg
jobs:
lint-code:
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: Run Linter
run: pnpm lint

View File

@@ -1,39 +0,0 @@
name: Linting Icons
on:
pull_request:
paths:
- 'icons/*'
jobs:
lint-filenames:
name: Lint Filenames
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: 'package.json'
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
files: icons/*
- name: Generate annotations
run: node ./scripts/lintFilenames.mts
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
lint-aliases:
name: Check Uniqueness of Aliases
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Check Uniqueness of Aliases
run: "! cat <(printf \"%s\\n\" icons/*.json | while read -r name; do basename \"$name\" .json; done) <(jq -cr 'select(.aliases) | .aliases[] | if type==\"string\" then . else .name end' icons/*.json) | sort | uniq -c | grep -ve '^\\s*1 '"

View File

@@ -1,15 +1,28 @@
name: Linting PR name: Linting
on: on:
pull_request: pull_request:
types: branches:
- opened - '**'
- edited
- reopened
jobs: jobs:
lint-pr-title: linting:
name: PR Title Lint runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Run Linter
run: pnpm lint
pr-title:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v5 - uses: amannn/action-semantic-pull-request@v5
@@ -26,7 +39,6 @@ jobs:
docs docs
ci ci
build build
chore
requireScope: true requireScope: true
ignoreLabels: | ignoreLabels: |
bot bot

View File

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

View File

@@ -3,6 +3,7 @@ name: Lucide font checks
on: on:
pull_request: pull_request:
paths: paths:
- icons/**
- tools/build-font/** - tools/build-font/**
- pnpm-lock.yaml - pnpm-lock.yaml
@@ -10,12 +11,12 @@ jobs:
lucide-font: lucide-font:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -27,7 +28,7 @@ jobs:
run: pnpm build:font run: pnpm build:font
- name: 'Upload to Artifacts' - name: 'Upload to Artifacts'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lucide-font name: lucide-font
path: lucide-font path: lucide-font

View File

@@ -4,7 +4,6 @@ on:
pull_request: pull_request:
paths: paths:
- packages/lucide-preact/** - packages/lucide-preact/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/** - tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
@@ -13,12 +12,12 @@ jobs:
lucide-preact: lucide-preact:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -4,37 +4,20 @@ on:
pull_request: pull_request:
paths: paths:
- packages/lucide-react-native/** - packages/lucide-react-native/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/** - tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
jobs: jobs:
build: lucide-react-native:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter lucide-react-native build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -4,7 +4,6 @@ on:
pull_request: pull_request:
paths: paths:
- packages/lucide-react/** - packages/lucide-react/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/** - tools/rollup-plugins/**
- scripts/generateNextJSAliases.mjs - scripts/generateNextJSAliases.mjs
@@ -14,12 +13,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -30,12 +29,12 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v3.8.1
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -1,24 +0,0 @@
name: Lucide Shared Checks
on:
pull_request:
paths:
- packages/shared/**
- pnpm-lock.yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-react test

View File

@@ -4,7 +4,6 @@ on:
pull_request: pull_request:
paths: paths:
- packages/lucide-solid/** - packages/lucide-solid/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/** - tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
@@ -13,12 +12,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -29,12 +28,12 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v3.8.1
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -11,12 +11,12 @@ jobs:
lucide-static: lucide-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -1,43 +0,0 @@
name: Lucide Svelte 5 checks
on:
pull_request:
paths:
- packages/svelte/**
- packages/shared/**
- tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter @lucide/svelte build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter @lucide/svelte test

View File

@@ -4,7 +4,6 @@ on:
pull_request: pull_request:
paths: paths:
- packages/lucide-svelte/** - packages/lucide-svelte/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/** - tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
@@ -13,12 +12,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -29,12 +28,12 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v3.8.1
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -4,7 +4,6 @@ on:
pull_request: pull_request:
paths: paths:
- packages/lucide-vue-next/** - packages/lucide-vue-next/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/** - tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
@@ -13,12 +12,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -29,12 +28,12 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v3.8.1
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@@ -1,29 +1,29 @@
name: Lucide Astro Checks name: Lucide Vue checks
on: on:
pull_request: pull_request:
paths: paths:
- packages/astro/** - packages/lucide-vue/**
- packages/shared/**
- tools/build-icons/** - tools/build-icons/**
- tools/rollup-plugins/**
- pnpm-lock.yaml - pnpm-lock.yaml
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version-file: 'package.json' node-version: 18
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build - name: Build
run: pnpm --filter @lucide/astro build run: pnpm --filter lucide-vue build
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -32,11 +32,11 @@ jobs:
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3.8.1 - uses: actions/setup-node@v3.8.1
with: with:
node-version-file: 'package.json' node-version: 18
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Test - name: Test
run: pnpm --filter @lucide/astro test run: pnpm --filter lucide-vue test

View File

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

View File

@@ -1,46 +0,0 @@
name: Pull request icon preview
on:
pull_request:
paths:
- 'icons/*.svg'
jobs:
generate-changed-icons-comment:
name: Generate Changed Icons Comment
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: 'package.json'
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
files: icons/*.svg
- uses: actions/setup-node@v6
- name: Install svgson for code preview (safer and faster than installing all deps)
run: npm install svgson@5.3.1 --force
- name: Save PR number
run: |
mkdir -p ./pr
echo ${{ github.event.number }} > ./pr/NR
- name: Generate comment markup
run: node ./scripts/generateChangedIconsCommentMarkup.mts >> ./pr/comment-markup.md
id: comment-markup
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
- uses: actions/upload-artifact@v4
with:
name: pr_number
path: pr/

View File

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

123
.github/workflows/pull-request.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Add Changed Icons comment
on:
pull_request_target:
paths:
- 'icons/*'
branches:
- main
- fix-icon-preview
jobs:
lint-filenames:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: icons/*
- name: Generate annotations
run: node ./scripts/lintFilenames.mjs
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
lint-contributors:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: icons/*
- uses: actions/setup-node@v4
- name: Install simple-git (safer and faster than installing all deps)
run: npm install simple-git
- name: Generate annotations
run: node ./scripts/updateContributors.mjs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FETCH_DEPTH: ${{ github.event.pull_request.commits }}
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
- name: Generate annotations
env:
ANNOTATION_SEVERITY: notice
ANNOTATION_TITLE: Contributors have changed!
ANNOTATION_DESCRIPTION: Don't add people who have only performed automatic optimizations.
run: |
git diff --unified=0 -- icons/*.json | # diff icon metadata (unified=0 gives the correct chunk line number)
perl -ne '/^(\+|- |@)/ && print' | # get chunks (lines that start with "+++", "@@", "+ ", "- ")
perl -pe 's/\n/%0A/' | # url encode line breaks (\n -> %0A)
perl -pe 's/%0A(\+\+\+ b\/)/\n\1/g' | # split chunks(one chunk per line)
perl -pe "s/\+\+\+ b\/([^@]*)%0A@@ -(\d+)[^\s]* \+(\d+)[^@]*@@(.*)/::$ANNOTATION_SEVERITY file=\1,line=\2,endLine=\3,title=$ANNOTATION_TITLE::$ANNOTATION_DESCRIPTION%0A\4/"
# Example for the previous substitution
# input: +++ b/icons/accessibility.json%0A@@ -2,0 +3 @@%0A+ "contributors": ["hi"],%0A@@ -13 +14 @@%0A+}%0A
# output: ::$ANNOTATION_SEVERITY file=icons/accessibility.json,line=2,endLine=3,title=$ANNOTATION_TITLE::$ANNOTATION_DESCRIPTION%0A%0A+ "contributors": ["hi"],%0A@@ -13 +14 @@%0A+}%0A
lint-aliases:
name: Check Uniqueness of Aliases
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Check Uniqueness of Aliases
run: "! cat <(printf \"%s\\n\" icons/*.json | while read -r name; do basename \"$name\" .json; done) <(jq -cr 'select(.aliases) | .aliases[] | if type==\"string\" then . else .name end' icons/*.json) | sort | uniq -c | grep -ve '^\\s*1 '"
generate-changed-icons-comment:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: icons/*.svg
- name: Find Comment
uses: peter-evans/find-comment@v2
id: pr-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Added or changed icons
- uses: actions/setup-node@v4
- name: Install svgson for code preview (safer and faster than installing all deps)
run: npm install svgson
- name: Generate comment markup
run: node ./scripts/generateChangedIconsCommentMarkup.mjs >> comment-markup.md
id: comment-markup
env:
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.pr-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: ./comment-markup.md
edit-mode: replace

View File

@@ -18,13 +18,9 @@ on:
description: Version description: Version
required: true required: true
permissions:
id-token: write # Required for OIDC
contents: write
jobs: jobs:
pre-release: 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 runs-on: ubuntu-latest
outputs: outputs:
VERSION: ${{ steps.get_version.outputs.VERSION }} VERSION: ${{ steps.get_version.outputs.VERSION }}
@@ -42,9 +38,6 @@ jobs:
if: github.repository == 'lucide-icons/lucide' if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre-release needs: pre-release
permissions:
id-token: write # Required for OIDC
contents: read
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -53,25 +46,27 @@ jobs:
'lucide', 'lucide',
'lucide-react', 'lucide-react',
'lucide-react-native', 'lucide-react-native',
'lucide-vue',
'lucide-vue-next', 'lucide-vue-next',
'lucide-angular', 'lucide-angular',
'lucide-preact', 'lucide-preact',
'lucide-solid', 'lucide-solid',
'lucide-svelte', 'lucide-svelte',
'@lucide/astro',
'@lucide/svelte',
] ]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile 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 - name: Set new version
run: pnpm --filter ${{ matrix.package }} version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version run: pnpm --filter ${{ matrix.package }} version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version
@@ -82,30 +77,27 @@ jobs:
run: pnpm --filter ${{ matrix.package }} test run: pnpm --filter ${{ matrix.package }} test
- name: Publish - name: Publish
run: pnpm --filter ${{ matrix.package }} publish --access public --no-git-checks --ignore-scripts run: pnpm --filter ${{ matrix.package }} publish --no-git-checks --ignore-scripts
env:
NPM_CONFIG_PROVENANCE: true
lucide-static: lucide-static:
if: github.repository == 'lucide-icons/lucide' if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [pre-release, lucide-font] needs: [pre-release, lucide-font]
permissions:
id-token: write # Required for OIDC
contents: read
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Set Auth Token
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
- name: Set new version - name: Set new version
run: pnpm --filter lucide-static version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version run: pnpm --filter lucide-static version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version
@@ -117,20 +109,18 @@ jobs:
- name: Publish - name: Publish
run: pnpm --filter lucide-static publish --no-git-checks run: pnpm --filter lucide-static publish --no-git-checks
env:
NPM_CONFIG_PROVENANCE: true
lucide-font: lucide-font:
if: github.repository == 'lucide-icons/lucide' if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre-release needs: pre-release
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v2
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 18
cache: 'pnpm' cache: 'pnpm'
node-version-file: 'package.json'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -142,7 +132,7 @@ jobs:
run: pnpm build:font run: pnpm build:font
- name: 'Upload to Artifacts' - name: 'Upload to Artifacts'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: lucide-font name: lucide-font
path: lucide-font path: lucide-font
@@ -151,19 +141,17 @@ jobs:
if: github.repository == 'lucide-icons/lucide' if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [pre-release, lucide-font] needs: [pre-release, lucide-font]
permissions:
id-token: write # Required for OIDC
contents: write
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v3
- name: Zip font and icons - name: Zip font and icons
run: | run: |
zip -r lucide-font-${{ needs.pre-release.outputs.VERSION }}.zip lucide-font zip -r lucide-font-${{ needs.pre-release.outputs.VERSION }}.zip lucide-font
zip -r lucide-icons-${{ needs.pre-release.outputs.VERSION }}.zip icons zip -r lucide-icons-${{ needs.pre-release.outputs.VERSION }}.zip icons
- name: Release zip and fonts - name: Release zip and fonts
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v1
with: with:
tag_name: ${{ needs.pre-release.outputs.VERSION }} tag_name: ${{ needs.pre-release.outputs.VERSION }}
files: | files: |

View File

@@ -1,33 +0,0 @@
name: 'Request Review'
on:
pull_request_target:
types: [opened]
paths:
- icons/*.svg
jobs:
request-review:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
with:
files: icons/*.svg
- run: |
while IFS= read -r file; do
jq -r '.contributors[]' "${file%.svg}.json"
done <<< "$CHANGED_FILES" | while read -r contributor; do
gh pr edit "${{ github.event.pull_request.number }}" --add-reviewer "$contributor" || true
done
env:
GH_TOKEN: ${{ github.token }}
CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}

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

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

9
.gitignore vendored
View File

@@ -4,7 +4,6 @@
.obsidian .obsidian
.now .now
.idea .idea
.env
node_modules node_modules
dist dist
build build
@@ -17,17 +16,11 @@ outlined
packages/**/src/icons/*.js packages/**/src/icons/*.js
packages/**/src/icons/*.ts packages/**/src/icons/*.ts
packages/**/src/icons/*.tsx packages/**/src/icons/*.tsx
packages/**/src/aliases/*.ts
packages/**/src/aliases.ts packages/**/src/aliases.ts
!packages/**/src/aliases/index.ts
packages/**/src/dynamicIconImports.ts packages/**/src/dynamicIconImports.ts
packages/**/DynamicIcon.d.ts
packages/**/dynamicIconImports.js packages/**/dynamicIconImports.js
packages/**/dynamicIconImports.d.ts packages/**/dynamicIconImports.d.ts
packages/**/dynamicIconImports.js.map packages/**/dynamicIconImports.js.map
packages/**/dynamic.d.ts
packages/**/dynamic.mjs.map
packages/**/dynamic.mjs
packages/**/LICENSE packages/**/LICENSE
categories.json categories.json
tags.json tags.json
@@ -44,7 +37,5 @@ docs/.vitepress/data/releaseMetaData
docs/.vitepress/data/categoriesData.json docs/.vitepress/data/categoriesData.json
docs/.vitepress/data/iconDetails docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json docs/.vitepress/data/relatedIcons.json
docs/.vitepress/data/brandStopwords.json
docs/.vercel docs/.vercel
docs/.nitro docs/.nitro
.gitignore

1
.npmrc Normal file
View File

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

View File

@@ -1,5 +1,4 @@
{ {
"$schema": "https://raw.githubusercontent.com/Yash-Singh1/vscode-snippets-json-schema/main/schema.json",
"Lucide SVG": { "Lucide SVG": {
"scope": "xml", "scope": "xml",
"description": "Base SVG with Lucide attributes.", "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|}\" />" "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": { "Ellipse": {
"scope": "xml", "scope": "xml",
"description": "SVG `ellipse`.", "description": "SVG `ellipse`.",

View File

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

View File

@@ -16,10 +16,10 @@ Guidelines for pull requests:
- __Make your commit messages as descriptive as possible.__ Include as much information as you can. Explain anything that the file diffs themselves wont make apparent. - __Make your commit messages as descriptive as possible.__ Include as much information as you can. Explain anything that the file diffs themselves wont make apparent.
- __Document your pull request__. Explain your fix, link to the relevant issue, add screenshots when adding new icons. - __Document your pull request__. Explain your fix, link to the relevant issue, add screenshots when adding new icons.
- __Make sure the target of your pull request is the relevant branch__. Most of bug fixes or new feature should go to the `main` branch. - __Make sure the target of your pull request is the relevant branch__. Most of bugfix or new feature should go to the `main` branch.
- __Include only related work__. If your pull request has unrelated commits, it won't be accepted. - __Include only related work__. If your pull request has unrelated commit, it won't be accepted.
### Icon Pull Requests ### Pull Requests Including Icons
#### Guidelines #### Guidelines
@@ -27,30 +27,26 @@ Please make sure you follow the icon guidelines, that should be followed to keep
Read it here: [ICON_GUIDELINES](https://lucide.dev/docs/icon-design-guide). Read it here: [ICON_GUIDELINES](https://lucide.dev/docs/icon-design-guide).
#### Lucide Studio ### Editor guides
For formatting and adjusting SVG icons, [@jguddas](https://github.com/jguddas) made a great tool called [Lucide Studio](https://studio.lucide.dev/). It is a web-based SVG editor that allows you to edit and adjust icons in the Lucide style. You can use it to create new icons or modify existing ones.
#### Editor guides
Here you can find instructions on how to implement the guidelines with different vector graphics editors: Here you can find instructions on how to implement the guidelines with different vector graphics editors:
##### [Adobe Illustrator Guide](https://lucide.dev/docs/illustrator-guide) #### [Adobe Illustrator Guide](https://lucide.dev/docs/illustrator-guide)
You can also [download an Adobe Illustrator template](https://github.com/lucide-icons/lucide/blob/main/docs/public/templates/illustrator_template.ai). You can also [download an Adobe Illustrator template](https://github.com/lucide-icons/lucide/blob/main/docs/public/templates/illustrator_template.ai).
##### [Inkscape Guide](https://lucide.dev/docs/inkscape-guide) #### [Inkscape Guide](https://lucide.dev/docs/inkscape-guide)
##### [Figma Guide](https://lucide.dev/docs/figma-guide) #### [Figma Guide](https://lucide.dev/docs/figma-guide)
##### [Affinity Designer Guide](https://lucide.dev/guide/design/affinity-designer-guide) #### [Affinity Designer Guide](https://lucide.dev/guide/design/affinity-designer-guide)
#### Submitting Multiple Icons ### Submitting Multiple Icons
If you want to submit multiple icons, please separate the icons and group them. That makes reviewing the icons easier and keeps the thread clean and scoped. If you want submit multiple icons, please separate the icons and group them. That makes reviewing the icons easier and keep the thread clean and scoped.
So don't submit multiple icons in one PR that have nothing to do with each other. So don't submit multiple icons in one PR that have noting to do with each other.
So for example don't create one PR with icons: `arrow-up`, `bicycle`, `arrow-down`. So for example don't create one PR with icons: `arrow-up`, `bicycle`, `arrow-down`.
Separate them into two PRs; 'pr-01' `arrow`, `arrow-down` and 'pr-02' `bicycle`. Seperate them by two PRs; 'pr-01' `arrow`, `arrow-down` and 'pr-02' `bicycle`.
## Icon Requests ## Icon Requests
@@ -81,7 +77,7 @@ To distribute different packages we use [PNPM workspaces](https://pnpm.io/worksp
The configured directory for workspaces is the [packages](https://github.com/lucide-icons/lucide/tree/main/packages) directory, located in the root directory. There you will find all the current packages from lucide. The configured directory for workspaces is the [packages](https://github.com/lucide-icons/lucide/tree/main/packages) directory, located in the root directory. There you will find all the current packages from lucide.
There are more workspaces defined, see [`pnpm-workspace.yaml`](https://github.com/lucide-icons/lucide/blob/main/pnpm-workspace.yaml). There are more workspaces defined, see [`pnpm-workspace.yaml`](https://github.com/lucide-icons/lucide/blob/main/pnpm-workspace.yaml).
> Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and uses pub for publishing. > Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and used pub for publishing.
### Generated Code ### Generated Code
@@ -131,7 +127,7 @@ When adding new features to for example the icon component for a framework. It i
### Local Testing ### Local Testing
To test changes in a local project, you can use `yarn link`, `npm link`, `bun link` or `pnpm link` to link the package. Before you do this make sure you've built the package first. To test changes in a local project, you can use `yarn link`, `npm link` or `pnpm link` to link the package. Before you do this make sure you builded the package first.
```sh ```sh
# in packages/lucide-react # in packages/lucide-react
@@ -161,30 +157,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. 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 ### Guides
Detailed documentation about: installation, guides, packages, design guides etc. Detailed documentation about: installation, guides, packages, design guides etc.
@@ -193,13 +165,15 @@ Detailed documentation about: installation, guides, packages, design guides etc.
All the icons of lucide in SVG format. These will be used as source for all the packages and other distributions for the lucide icons. All the icons of lucide in SVG format. These will be used as source for all the packages and other distributions for the lucide icons.
### Packages ### packages
Includes all the (npm) packages of lucide. Includes all the (npm) packages of lucide.
### Scripts > Note: One package is not managed by pnpm: **lucide-flutter**, this package is written in Dart and used pub for publishing.
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. ### scripts
Includes usefully scripts to automate certain jobs. Big part of the scripts is the template generation, for example it generates icon components for all the packages. These scripts are usually executed from the "scripts" section in the package.json.
## Documentation ## Documentation
@@ -216,4 +190,4 @@ If you need any help or have problems with you contribution. Please don't hesita
Thank you to all the people who already contributed to Lucide! Thank you to all the people who already contributed to Lucide!
<a href="https://github.com/lucide-icons/lucide/graphs/contributors"> <a href="https://github.com/lucide-icons/lucide/graphs/contributors">
<img src="https://opencollective.com/lucide-icons/contributors.svg?width=800" /></a> <img src="https://opencollective.com/lucide-icons/contributors.svg?width=890" /></a>

26
LICENSE
View File

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

276
README.md
View File

@@ -8,40 +8,185 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/lucide-icons/lucide/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/lucide" alt="license"></a> <a href="https://github.com/lucide-icons/lucide/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/lucide" alt="license"></a>
<a href="https://www.figma.com/community/plugin/939567362549682242/Lucide-Icons"><img src="https://img.shields.io/badge/Figma-F24E1E?logo=figma&logoColor=white" alt="figma installs"></a> <a href="https://www.npmjs.com/package/lucide"><img src="https://img.shields.io/npm/v/lucide" alt="npm package"></a>
<a href="https://www.figma.com/community/plugin/939567362549682242/Lucide-Icons"><img src="https://img.shields.io/endpoint?logo=figma&label=installs&url=https://yuanqing.github.io/figma-plugins-stats/plugin/939567362549682242/installs.json" alt="figma installs"></a>
<a href="https://github.com/lucide-icons/lucide/actions/workflows/release.yml"><img src="https://github.com/lucide-icons/lucide/actions/workflows/release.yml/badge.svg" alt="build status"></a> <a href="https://github.com/lucide-icons/lucide/actions/workflows/release.yml"><img src="https://github.com/lucide-icons/lucide/actions/workflows/release.yml/badge.svg" alt="build status"></a>
<a href="https://discord.gg/EH6nSts"><img src="https://img.shields.io/discord/723074157486800936?label=chat&logo=discord&logoColor=%23ffffff&colorB=%237289DA" alt="discord chat"></a> <a href="https://discord.gg/EH6nSts"><img src="https://img.shields.io/discord/723074157486800936?label=chat&logo=discord&logoColor=%23ffffff&colorB=%237289DA" alt="discord chat"></a>
</p> </p>
<p align="center">
<a href="https://lucide.dev/icons/">Icons</a>
·
<a href="https://lucide.dev/guide/">Guide</a>
·
<a href="https://lucide.dev/packages">Packages</a>
·
<a href="https://lucide.dev/license">License</a>
·
<a href="https://lucide.dev/showcase">Showcase</a>
</p>
# Lucide # 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](https://lucide.dev/packages) to make it easier to use these icons in your project. Community-run fork of [Feather Icons](https://github.com/feathericons/feather), open for anyone to contribute icons.
## Packages It began after growing dissatisfaction with the [Feather Icons](https://github.com/feathericons/feather) project moderation. With over 300+ open issues and over 100+ open PRs, the Feather Icons project has been abandoned. This unfortunately means that hundreds of developers and designers wasted their time contributing to Feather Icons with no chance of PRs being accepted.
| Logo | Package | Version | Downloads | Links | Lucide is trying to expand the icon set as much as possible while staying faithful to the original simplistic design language. We do this as a community of devs and designers and hope that you'll join us!
| ---- | ------- | ------- | --------- | ----- |
| <img src="https://lucide.dev/framework-logos/js.svg" alt="JS logo" width="48"> | **`lucide`** | [![npm](https://img.shields.io/npm/v/lucide)](https://www.npmjs.com/package/lucide) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide) | [Docs](https://lucide.dev/guide/packages/lucide) · [Source](./packages/lucide) | ### Why choose Lucide over Feather Icons
| <img src="https://lucide.dev/framework-logos/react.svg" alt="React logo" width="48"> | **`lucide-react`** | [![npm](https://img.shields.io/npm/v/lucide-react)](https://www.npmjs.com/package/lucide-react) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-react) | [Docs](https://lucide.dev/guide/packages/lucide-react) · [Source](./packages/lucide-react) |
| <img src="https://lucide.dev/framework-logos/vue.svg" alt="Vue logo" width="48"> | **`lucide-vue-next`** | [![npm](https://img.shields.io/npm/v/lucide-vue-next)](https://www.npmjs.com/package/lucide-vue-next) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-vue-next) | [Docs](https://lucide.dev/guide/packages/lucide-vue-next) · [Source](./packages/lucide-vue-next) | - More icons to work with: Lucide already has hundreds of icons more than Feather does.
| <img src="https://lucide.dev/framework-logos/svelte.svg" alt="Svelte logo" width="48"> | **`lucide-svelte`** | [![npm](https://img.shields.io/npm/v/lucide-svelte)](https://www.npmjs.com/package/lucide-svelte) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-svelte) | [Docs](https://lucide.dev/guide/packages/lucide-svelte) · [Source](./packages/lucide-svelte) | - Official libraries and integrations with popular frameworks and design tools.
| <img src="https://lucide.dev/framework-logos/solid.svg" alt="Solid logo" width="48"> | **`lucide-solid`** | [![npm](https://img.shields.io/npm/v/lucide-solid)](https://www.npmjs.com/package/lucide-solid) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-solid) | [Docs](https://lucide.dev/guide/packages/lucide-solid) · [Source](./packages/lucide-solid) | - Well maintained code base.
| <img src="https://lucide.dev/framework-logos/preact.svg" alt="Preact logo" width="48"> | **`lucide-preact`** | [![npm](https://img.shields.io/npm/v/lucide-preact)](https://www.npmjs.com/package/lucide-preact) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-preact) | [Docs](https://lucide.dev/guide/packages/lucide-preact) · [Source](./packages/lucide-preact) | - Active community, regularly growing and improving the set.
| <img src="https://lucide.dev/framework-logos/react-native.svg" alt="React Native logo" width="48"> | **`lucide-react-native`** | [![npm](https://img.shields.io/npm/v/lucide-react-native)](https://www.npmjs.com/package/lucide-react-native) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-react-native) | [Docs](https://lucide.dev/guide/packages/lucide-react-native) · [Source](./packages/lucide-react-native) |
| <img src="https://lucide.dev/framework-logos/angular.svg" alt="Angular logo" width="48"> | **`lucide-angular`** | [![npm](https://img.shields.io/npm/v/lucide-angular)](https://www.npmjs.com/package/lucide-angular) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-angular) | [Docs](https://lucide.dev/guide/packages/lucide-angular) · [Source](./packages/lucide-angular) | ## Table of Contents
| <img src="https://lucide.dev/framework-logos/astro.svg" alt="Astro logo" width="48"> | **`@lucide/astro`** | [![npm](https://img.shields.io/npm/v/@lucide/astro)](https://www.npmjs.com/package/@lucide/astro) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/astro) | [Docs](https://lucide.dev/guide/packages/lucide-astro) · [Source](./packages/astro) |
| <img src="https://lucide.dev/framework-logos/svg.svg" alt="SVG logo" width="48"> | **`lucide-static`** | [![npm](https://img.shields.io/npm/v/lucide-static)](https://www.npmjs.com/package/lucide-static) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-static) | [Docs](https://lucide.dev/guide/packages/lucide-static) · [Source](./packages/lucide-static) | - [Usage](#usage)
- [Web](#web)
- [React](#react)
- [React Native](#react-native)
- [Vue 2](#vue-2)
- [Vue 3](#vue-3)
- [Angular](#angular)
- [Preact](#preact)
- [Static (svg sprite, font, icons ..)](#static-svg-sprite-font-icons-)
- [Figma](#figma)
- [Laravel](#laravel)
- [Svelte](#svelte)
- [Solid](#solid)
- [Hyva](#hyva)
- [Eleventy](#eleventy)
- [Contributing](#contributing)
- [Community](#community)
- [License](#license)
- [Credits](#credits)
- [Sponsors](#sponsors)
## Usage
At its core, Lucide is a collection of [SVG](https://svgontheweb.com/#svg) files. This means that you can use Lucide icons in all the same ways you can use SVGs (e.g. `img`, `background-image`, `inline`, `object`, `embed`, `iframe`). Here's a helpful article detailing the many ways SVGs can be used on the web: [SVG on the Web Implementation Options](https://svgontheweb.com/#implementation)
The following are additional ways you can use Lucide.
With the Javascript library you can easily incorporate the icon you want in your webpage.
### Web
Implementation of the lucide icon library for web applications.
```sh
npm install lucide
```
or
```sh
yarn add lucide
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide#lucide).
### React
Implementation of the lucide icon library for react applications.
```sh
yarn add lucide-react
```
or
```sh
npm install lucide-react
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-react#lucide-react).
### React Native
Implementation of the lucide icon library for React Native applications.
```sh
yarn add lucide-react-native
```
or
```sh
npm install lucide-react-native
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-react-native#lucide-react-native).
### Vue 2
Implementation of the lucide icon library for vue applications.
```sh
yarn add lucide-vue
```
or
```sh
npm install lucide-vue
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-vue#lucide-vue).
### Vue 3
Implementation of the lucide icon library for vue applications.
```sh
yarn add lucide-vue-next
```
or
```sh
npm install lucide-vue-next
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-vue-next#lucide-vue-next).
### Angular
```sh
yarn add lucide-angular
```
or
```sh
npm install lucide-angular
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-angular#lucide-angular).
### Preact
Implementation of the lucide icon library for preact applications.
```sh
yarn add lucide-preact
```
or
```sh
npm install lucide-preact
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-preact#lucide-preact).
### Static (svg sprite, font, icons ..)
Assets:
[Font Files](https://github.com/lucide-icons/lucide/releases/latest)
[SVG Files](https://github.com/lucide-icons/lucide/releases/latest)
[SVG Sprite](https://cdn.jsdelivr.net/npm/lucide-static@latest/sprite.svg)
NPM package
```sh
yarn add lucide-static
```
or
```sh
npm install lucide-static
```
### Figma ### Figma
@@ -51,34 +196,88 @@ Visit [Figma community page](https://www.figma.com/community/plugin/939567362549
<img width="420" src="https://www.figma.com/community/plugin/939567362549682242/thumbnail" alt="Figma Lucide Cover"> <img width="420" src="https://www.figma.com/community/plugin/939567362549682242/thumbnail" alt="Figma Lucide Cover">
### Laravel
Implementation of Lucide icon's using `blade-icons` for Laravel based projects.
```sh
composer require mallardduck/blade-lucide-icons
```
For more details, see the [documentation](https://github.com/mallardduck/blade-lucide-icons/blob/main/README.md).
### Svelte
Implementation of the lucide icon library for Svelte applications.
```sh
yarn add lucide-svelte
```
or
```sh
npm install lucide-svelte
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-svelte#lucide-svelte).
### Solid
Implementation of the lucide icon library for solid applications.
```sh
yarn add lucide-solid
```
or
```sh
npm install lucide-solid
```
For more details, see the [documentation](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-solid#lucide-solid).
### Hyva
Implementation of Lucide icon's using Hyvä's svg php viewmodal to render icons for Magento 2 Hyva theme based projects.
```sh
composer require siteation/magento2-hyva-icons-lucide
```
For more details, see the [documentation](https://github.com/Siteation/magento2-hyva-icons-lucide/blob/main/README.md).
### Eleventy
Using this plugin, Eleventy projects can incorporate Lucide icons. it makes it simple to use Lucide icons into your themes via shortcodes, improving your website's overall usability and visual appeal.
```sh
npm install @grimlink/eleventy-plugin-lucide-icons
```
For more details, see the [documentation](https://github.com/GrimLink/eleventy-plugin-lucide-icons/blob/main/README.md).
## Contributing ## Contributing
For more info on how to contribute please see the [contribution guidelines](https://github.com/lucide-icons/lucide/blob/main/CONTRIBUTING.md). For more info on how to contribute please see the [contribution guidelines](https://github.com/lucide-icons/lucide/blob/main/CONTRIBUTING.md).
Caught a mistake or want to contribute to the documentation? [Edit this page on Github](https://github.com/lucide-icons/lucide/blob/main/README.md) 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 ## Community
Join the community on our [Discord](https://discord.gg/EH6nSts) server! Join the community on our [Discord](https://discord.gg/EH6nSts) server!
## License ## License
Lucide is totally free for commercial use and personal use, this software is licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE). Lucide is totally free for commercial use and personally use, this software is licensed under the [ISC License](https://github.com/lucide-icons/lucide/blob/main/LICENSE).
## Credits ## Credits
Thank you to all the people who contributed to Lucide! Thank you to all the people who contributed to Lucide!
<a href="https://github.com/lucide-icons/lucide/graphs/contributors"> <a href="https://github.com/lucide-icons/lucide/graphs/contributors">
<img src="https://opencollective.com/lucide-icons/contributors.svg?width=890" /></a>
<img src="https://opencollective.com/lucide-icons/contributors.svg?width=800" />
</a>
## Sponsors ## Sponsors
@@ -88,7 +287,6 @@ 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> <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>
### Awesome backers 🍺 ### Awesome backer 🍺
<a href="https://github.com/pdfme/pdfme"><img src="docs/public/sponsors/pdfme.svg" width="180" alt="pdfme sponsor badge" /></a> <a href="https://www.scipress.io?utm_source=lucide"><img src="docs/public/sponsors/scipress.svg" width="180" alt="Scipress sponsor badge" /></a>
<a href="https://www.fina.money/"><img src="docs/public/sponsors/fina-money.png" width="180" alt="Fina Money sponsor badge" /></a>

View File

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

5
categories/currency.json Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Currency",
"icon": "dollar-sign"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Furniture",
"icon": "rocking-chair"
}

5
categories/maps.json Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Maps",
"icon": "map"
}

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "../category.schema.json", "$schema": "../category.schema.json",
"title": "Mathematics", "title": "Maths",
"icon": "divide" "icon": "divide"
} }

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "../category.schema.json", "$schema": "../category.schema.json",
"title": "Finance", "title": "Money",
"icon": "piggy-bank" "icon": "piggy-bank"
} }

View File

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

View File

@@ -1,5 +1,5 @@
{ {
"$schema": "../category.schema.json", "$schema": "../category.schema.json",
"title": "Notification", "title": "Notifications",
"icon": "triangle-alert" "icon": "triangle-alert"
} }

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,10 @@
"name": "connectivity", "name": "connectivity",
"title": "Connectivity" "title": "Connectivity"
}, },
{
"name": "currency",
"title": "Currency"
},
{ {
"name": "cursors", "name": "cursors",
"title": "Cursors" "title": "Cursors"
@@ -59,14 +63,14 @@
"name": "files", "name": "files",
"title": "File icons" "title": "File icons"
}, },
{
"name": "finance",
"title": "Finance"
},
{ {
"name": "food-beverage", "name": "food-beverage",
"title": "Food & beverage" "title": "Food & beverage"
}, },
{
"name": "furniture",
"title": "Furniture"
},
{ {
"name": "gaming", "name": "gaming",
"title": "Gaming" "title": "Gaming"
@@ -84,13 +88,21 @@
"title": "Mail" "title": "Mail"
}, },
{ {
"name": "math", "name": "maps",
"title": "Mathematics" "title": "Maps"
},
{
"name": "maths",
"title": "Maths"
}, },
{ {
"name": "medical", "name": "medical",
"title": "Medical" "title": "Medical"
}, },
{
"name": "money",
"title": "Money"
},
{ {
"name": "multimedia", "name": "multimedia",
"title": "Multimedia" "title": "Multimedia"
@@ -101,11 +113,11 @@
}, },
{ {
"name": "navigation", "name": "navigation",
"title": "Navigation, Maps, and POIs" "title": "Navigation"
}, },
{ {
"name": "notifications", "name": "notifications",
"title": "Notification" "title": "Notifications"
}, },
{ {
"name": "people", "name": "people",

View File

@@ -7,14 +7,6 @@
"dark": "/company-logos/vercel-dark.svg" "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", "name": "Supabase",
"url": "https://supabase.com", "url": "https://supabase.com",
@@ -31,14 +23,6 @@
"dark": "/company-logos/obsidian-dark.svg" "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", "name": "Open Collective",
"url": "https://opencollective.com", "url": "https://opencollective.com",

View File

@@ -14,13 +14,5 @@
"light": "/library-logos/tamagui.svg", "light": "/library-logos/tamagui.svg",
"dark": "/library-logos/tamagui.svg" "dark": "/library-logos/tamagui.svg"
} }
},
{
"name": "Reflex",
"url": "https://reflex.dev",
"image": {
"light": "/library-logos/reflex-light.svg",
"dark": "/library-logos/reflex-dark.svg"
}
} }
] ]

View File

@@ -31,8 +31,24 @@
} }
] ]
}, },
"lucide-vue-next": { "lucide-vue": {
"order": 2, "order": 2,
"icon": "vue",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-vue",
"href": "https://www.npmjs.com/package/lucide-vue"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-vue",
"href": "https://www.npmjs.com/package/lucide-vue"
}
]
},
"lucide-vue-next": {
"order": 3,
"icon": "vue-next", "icon": "vue-next",
"shields": [ "shields": [
{ {
@@ -48,7 +64,7 @@
] ]
}, },
"lucide-svelte": { "lucide-svelte": {
"order": 3, "order": 4,
"icon": "svelte", "icon": "svelte",
"shields": [ "shields": [
{ {
@@ -79,8 +95,24 @@
} }
] ]
}, },
"lucide-react-native": { "lucide-preact": {
"order": 5, "order": 5,
"icon": "preact",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-preact",
"href": "https://www.npmjs.com/package/lucide-preact"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-preact",
"href": "https://www.npmjs.com/package/lucide-preact"
}
]
},
"lucide-react-native": {
"order": 6,
"icon": "react-native", "icon": "react-native",
"shields": [ "shields": [
{ {
@@ -96,7 +128,7 @@
] ]
}, },
"lucide-angular": { "lucide-angular": {
"order": 6, "order": 7,
"icon": "angular", "icon": "angular",
"shields": [ "shields": [
{ {
@@ -111,43 +143,8 @@
} }
] ]
}, },
"lucide-preact": {
"order": 7,
"icon": "preact",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-preact",
"href": "https://www.npmjs.com/package/lucide-preact"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-preact",
"href": "https://www.npmjs.com/package/lucide-preact"
}
]
},
"@lucide/astro": {
"docsAlias": "lucide-astro",
"packageDirname": "astro",
"order": 8,
"icon": "astro",
"iconDark": "astro-dark",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/@lucide/astro",
"href": "https://www.npmjs.com/package/@lucide/astro"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/@lucide/astro",
"href": "https://www.npmjs.com/package/@lucide/astro"
}
]
},
"lucide-static": { "lucide-static": {
"order": 9, "order": 8,
"icon": "svg", "icon": "svg",
"shields": [ "shields": [
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { createLucideIcon } from 'lucide-react/src/lucide-react'; 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 { IconEntity } from '../theme/types';
import { renderToStaticMarkup } from 'react-dom/server'; import { renderToStaticMarkup } from 'react-dom/server';
import { IconContent } from './generateZip'; import { IconContent } from './generateZip';

View File

@@ -46,10 +46,6 @@ const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
text: 'Filled icons', text: 'Filled icons',
link: '/guide/advanced/filled-icons', link: '/guide/advanced/filled-icons',
}, },
{
text: 'Aliased Names',
link: '/guide/advanced/aliased-names',
},
// { // {
// text: 'Combining icons', // text: 'Combining icons',
// }, // },
@@ -72,6 +68,10 @@ const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
text: 'Lucide React', text: 'Lucide React',
link: '/guide/packages/lucide-react', link: '/guide/packages/lucide-react',
}, },
{
text: 'Lucide React Native',
link: '/guide/packages/lucide-react-native',
},
{ {
text: 'Lucide Vue', text: 'Lucide Vue',
link: '/guide/packages/lucide-vue-next', link: '/guide/packages/lucide-vue-next',
@@ -84,21 +84,13 @@ const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
text: 'Lucide Solid', text: 'Lucide Solid',
link: '/guide/packages/lucide-solid', link: '/guide/packages/lucide-solid',
}, },
{
text: 'Lucide React Native',
link: '/guide/packages/lucide-react-native',
},
{
text: 'Lucide Angular',
link: '/guide/packages/lucide-angular',
},
{ {
text: 'Lucide Preact', text: 'Lucide Preact',
link: '/guide/packages/lucide-preact', link: '/guide/packages/lucide-preact',
}, },
{ {
text: 'Lucide Astro', text: 'Lucide Angular',
link: '/guide/packages/lucide-astro', link: '/guide/packages/lucide-angular',
}, },
{ {
text: 'Lucide Static', text: 'Lucide Static',

View File

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

View File

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

View File

@@ -5,6 +5,7 @@
</template> </template>
<style scoped> <style scoped>
.icon-button { .icon-button {
display: inline-flex; display: inline-flex;
border: 1px solid transparent; border: 1px solid transparent;

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ export default {
} }
return null; return null;
}) })
.then((res) => res?.tag_name); .then((res) => res.tag_name);
return { return {
version, version,

View File

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

View File

@@ -48,7 +48,6 @@ function resetStyle () {
color.value = 'currentColor' color.value = 'currentColor'
strokeWidth.value = 2 strokeWidth.value = 2
size.value = 24 size.value = 24
absoluteStrokeWidth.value = false
} }
watch(absoluteStrokeWidth, (enabled) => { watch(absoluteStrokeWidth, (enabled) => {
@@ -172,9 +171,11 @@ watch(absoluteStrokeWidth, (enabled) => {
margin-top: 32px; margin-top: 32px;
padding: 0; padding: 0;
background: none; background: none;
max-width: 280px;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.card { .card {
display: grid; display: grid;
grid-template-columns: 8fr 10fr; grid-template-columns: 8fr 10fr;

View File

@@ -22,32 +22,31 @@ export default {
logo: '/framework-logos/svelte.svg', logo: '/framework-logos/svelte.svg',
label: 'Lucide documentation for Svelte', label: 'Lucide documentation for Svelte',
}, },
{
name: 'lucide-solid',
logo: '/framework-logos/solid.svg',
label: 'Lucide documentation for Solid',
},
{ {
name: 'lucide-preact', name: 'lucide-preact',
logo: '/framework-logos/preact.svg', logo: '/framework-logos/preact.svg',
label: 'Lucide documentation for Preact', label: 'Lucide documentation for Preact',
}, },
{
name: 'lucide-solid',
logo: '/framework-logos/solid.svg',
label: 'Lucide documentation for Solid',
},
{ {
name: 'lucide-angular', name: 'lucide-angular',
logo: '/framework-logos/angular.svg', logo: '/framework-logos/angular.svg',
label: 'Lucide documentation for Angular', label: 'Lucide documentation for Angular',
}, },
{
name: 'lucide-astro',
logo: '/framework-logos/astro.svg',
logoDark: '/framework-logos/astro-dark.svg',
label: 'Lucide documentation for Astro',
},
{ {
name: 'lucide-react-native', name: 'lucide-react-native',
logo: '/framework-logos/react-native.svg', logo: '/framework-logos/react-native.svg',
label: 'Lucide documentation for React Native', label: 'Lucide documentation for React Native',
}, },
{
name: 'lucide-flutter',
logo: '/framework-logos/flutter.svg',
label: 'Lucide documentation for Flutter',
},
], ],
}; };
}, },

View File

@@ -13,7 +13,7 @@ const { go } = useRouter()
<HomeSectionTitle>Available For:</HomeSectionTitle> <HomeSectionTitle>Available For:</HomeSectionTitle>
<div class="packages-list"> <div class="packages-list">
<a <a
v-for="{ name, logo, logoDark } in data.packages" v-for="{ name, logo } in data.packages"
:href="`/guide/packages/${name}`" :href="`/guide/packages/${name}`"
class="package-logo" class="package-logo"
:aria-label="`Read more about: ${name} package`" :aria-label="`Read more about: ${name} package`"
@@ -21,17 +21,10 @@ const { go } = useRouter()
> >
<img <img
:src="logo" :src="logo"
:class="{ light: logoDark, 'image-logo': true }" height="36"
:alt="`${name} logo`" width="36"
loading="lazy" loading="lazy"
/>
<img
v-if="logoDark"
:src="logoDark"
:alt="`${name} logo`" :alt="`${name} logo`"
class="image-logo dark"
loading="lazy"
/> />
</a> </a>
</div> </div>
@@ -42,13 +35,6 @@ const { go } = useRouter()
</template> </template>
<style scoped> <style scoped>
.image-logo {
object-fit: contain;
width: 36px;
height: 36px;
}
.packages-list { .packages-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -71,11 +57,4 @@ const { go } = useRouter()
.package-logo:hover { .package-logo:hover {
opacity: .6; opacity: .6;
} }
html.dark .image-logo.light {
display: none;
}
html:not(.dark) .image-logo.dark {
display: none;
}
</style> </style>

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,6 @@ import {useData, useRouter} from 'vitepress';
import { computed } from 'vue'; import { computed } from 'vue';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'; import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { diamond } from '../../../data/iconNodes' import { diamond } from '../../../data/iconNodes'
import deprecationReasonTemplate from '../../../../../tools/build-icons/utils/deprecationReasonTemplate.ts';
const props = defineProps<{ const props = defineProps<{
icon: IconEntity icon: IconEntity
@@ -26,15 +24,6 @@ const tags = computed(() => {
}) })
const DiamondIcon = createLucideIcon('Diamond', diamond) const DiamondIcon = createLucideIcon('Diamond', diamond)
const deprecatedTitle = computed(() => {
if (!props.icon.deprecationReason) return '';
return deprecationReasonTemplate(props.icon.deprecationReason, {
componentName: props.icon.name,
iconName: props.icon.name,
toBeRemovedInVersion: props.icon.toBeRemovedInVersion,
});
});
</script> </script>
<template> <template>
@@ -47,13 +36,6 @@ const deprecatedTitle = computed(() => {
<DiamondIcon fill="currentColor" :size="12"/> <DiamondIcon fill="currentColor" :size="12"/>
{{ icon.externalLibrary }} {{ icon.externalLibrary }}
</div> </div>
<Badge
v-if="icon.deprecated"
class="deprecated-badge"
:title="deprecatedTitle"
>
Deprecated
</Badge>
</div> </div>
<div class="tags-scroller" v-if="tags.length"> <div class="tags-scroller" v-if="tags.length">
<p class="icon-tags horizontal-scroller"> <p class="icon-tags horizontal-scroller">
@@ -116,16 +98,6 @@ const deprecatedTitle = computed(() => {
align-items: center; align-items: center;
} }
.deprecated-badge {
background-color: var(--vp-c-brand-5);
margin-left: 40px;
opacity: .8;
}
.deprecated-badge:hover {
background-color: var(--vp-c-brand-2);
}
.icon-tags { .icon-tags {
font-size: 16px; font-size: 16px;
color: var(--vp-c-text-2); color: var(--vp-c-text-2);

View File

@@ -178,8 +178,6 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
stroke-width: var(--customize-strokeWidth, 2); stroke-width: var(--customize-strokeWidth, 2);
width: calc(var(--customize-size, 24) * 1px); width: calc(var(--customize-size, 24) * 1px);
height: 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 { html.absolute-stroke-width .lucide-icon.customizable {

View File

@@ -26,9 +26,10 @@ const iconComponent = computed(() => {
<component <component
ref="previewIcon" ref="previewIcon"
:is="iconComponent" :is="iconComponent"
:size="size" :width="size"
:color="color" :height="size"
:strokeWidth="absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth" :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"> <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"> <g :key="`grid-${i}`" v-for="(_, i) in gridLines">

View File

@@ -1,438 +0,0 @@
<script setup lang="ts">
import type { IconEntity } from '../../types';
import { computed } from 'vue';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon.ts';
import Calendar from '../../../data/iconDetails/calendar.ts';
import Clock from '../../../data/iconDetails/clock.ts';
import Bug from '../../../data/iconDetails/bug.ts';
import Rocket from '../../../data/iconDetails/rocket.ts';
import TriangleAlert from '../../../data/iconDetails/triangle-alert.ts';
import PartyPopper from '../../../data/iconDetails/party-popper.ts';
import Scissors from '../../../data/iconDetails/scissors.ts';
import Copy from '../../../data/iconDetails/copy.ts';
import Save from '../../../data/iconDetails/save.ts';
import Clipboard from '../../../data/iconDetails/clipboard.ts';
import MessageCircle from '../../../data/iconDetails/message-circle.ts';
import ThumbsDown from '../../../data/iconDetails/thumbs-down.ts';
import ThumbsUp from '../../../data/iconDetails/thumbs-up.ts';
import Heart from '../../../data/iconDetails/heart.ts';
import Folder from '../../../data/iconDetails/folder.ts';
import Files from '../../../data/iconDetails/files.ts';
import Plus from '../../../data/iconDetails/plus.ts';
import File from '../../../data/iconDetails/file.ts';
import FileText from '../../../data/iconDetails/file-text.ts';
const props = defineProps<{
name: IconEntity['name'];
iconNode: IconEntity['iconNode'];
}>();
const iconComponent = computed(() => {
if (!props.name || !props.iconNode) return null;
return createLucideIcon(props.name, props.iconNode);
});
const CalendarIcon = createLucideIcon('calendar', Calendar.iconNode);
const ClockIcon = createLucideIcon('clock', Clock.iconNode);
const BugIcon = createLucideIcon('bug', Bug.iconNode);
const RocketIcon = createLucideIcon('rocket', Rocket.iconNode);
const AlertTriangleIcon = createLucideIcon('alert-triangle', TriangleAlert.iconNode);
const ScissorsIcon = createLucideIcon('scissors', Scissors.iconNode);
const CopyIcon = createLucideIcon('copy', Copy.iconNode);
const SaveIcon = createLucideIcon('save', Save.iconNode);
const ClipboardIcon = createLucideIcon('clipboard', Clipboard.iconNode);
const PartyPopperIcon = createLucideIcon('party-popper', PartyPopper.iconNode);
const HeartIcon = createLucideIcon('heart', Heart.iconNode);
const ThumbsUpIcon = createLucideIcon('thumbs-up', ThumbsUp.iconNode);
const ThumbsDownIcon = createLucideIcon('thumbs-down', ThumbsDown.iconNode);
const MessageCircleIcon = createLucideIcon('message-circle', MessageCircle.iconNode);
const FolderIcon = createLucideIcon('folder.ts', Folder.iconNode);
const FilesIcon = createLucideIcon('files.ts', Files.iconNode);
const PlusIcon = createLucideIcon('plus.ts', Plus.iconNode);
const FileIcon = createLucideIcon('file.ts', File.iconNode);
const FileTextIcon = createLucideIcon('file-text.ts', FileText.iconNode);
const prettyName = props.name
.split('-')
.at(0)
.split('')
.map((character, index) => (index === 0 ? character.toUpperCase() : character))
.join('');
</script>
<template>
<section class="showcase">
<h2 class="title">See this icon in action</h2>
<div class="showcase-grid">
<div class="showcase-item column">
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder narrow"></div>
<div class="spacer"></div>
<div class="actions">
<button class="button button-brand">
<iconComponent />
{{ prettyName }}
</button>
<button class="button button-alt">Cancel</button>
</div>
</div>
<div class="showcase-item column">
<div class="placeholder narrow"></div>
<div class="input-wrapper">
<CalendarIcon v-if="name !== 'calendar'" />
<ClockIcon v-if="name == 'calendar'" />
<input
type="text"
v-if="name !== 'calendar'"
placeholder="Enter a date..."
/>
<input
type="text"
v-if="name == 'calendar'"
placeholder="Enter a time..."
/>
</div>
<div class="spacer"></div>
<div class="placeholder narrow"></div>
<div class="input-wrapper">
<iconComponent />
<input
type="text"
placeholder="Enter a value..."
/>
</div>
</div>
<div class="showcase-item column">
<div
class="row"
v-if="name !== 'bug'"
>
<div class="placeholder"></div>
<div class="badge badge-red">
<BugIcon :size="20" />
Bug
</div>
</div>
<div
class="row"
v-else
>
<div class="placeholder"></div>
<div class="badge badge-red">
<AlertTriangleIcon :size="20" />
Alert
</div>
</div>
<div class="row">
<div class="placeholder"></div>
<div class="badge badge-indigo">
<RocketIcon
:size="20"
v-if="name !== 'rocket'"
/>
<PartyPopperIcon
:size="20"
v-else
/>
Feature
</div>
</div>
<div class="row">
<div class="placeholder"></div>
<div class="badge badge-green">
<iconComponent :size="20" />
{{ prettyName }}
</div>
</div>
</div>
<div class="showcase-item column">
<button class="button button-alt button-square">
<FolderIcon v-if="name !== 'folder'" />
<FilesIcon v-else />
Documents
<PlusIcon class="ms-auto" />
</button>
<button class="button button-alt button-square">
<FileIcon v-if="name !== 'file'" />
<FileTextIcon v-else />
Readme
</button>
<button class="button button-alt button-square">
<iconComponent />
{{ prettyName }}
<span class="badge-notification ms-auto">12</span>
</button>
</div>
<div class="showcase-item column">
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder narrow"></div>
<div class="spacer"></div>
<div class="actions">
<div class="icon-counter">
<HeartIcon v-if="name !== 'heart'" />
<ThumbsUpIcon v-else />
112
</div>
<div class="spacer"></div>
<div class="icon-counter">
<MessageCircleIcon v-if="name !== 'message-circle'" />
<ThumbsDownIcon v-else />
8
</div>
<div class="spacer"></div>
<div class="icon-counter">
<iconComponent />
11
</div>
</div>
</div>
<div class="showcase-item">
<div class="column align-items-center">
<div class="actions justify-content-center">
<button class="button button-icon">
<CopyIcon v-if="name !== 'copy'" />
<SaveIcon v-else />
</button>
<button class="button button-icon">
<ScissorsIcon v-if="name !== 'scissors'" />
<SaveIcon v-else />
</button>
<button class="button button-icon">
<ClipboardIcon v-if="name !== 'clipboard'" />
<SaveIcon v-else />
</button>
<button class="button button-icon">
<iconComponent></iconComponent>
<span class="badge-notification">2</span>
</button>
</div>
<div class="spacer"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.showcase {
margin-bottom: 32px;
}
.showcase-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 48px;
margin-inline-start: 24px;
margin-block-start: 48px;
}
.showcase-item {
padding: 24px;
border-radius: 8px;
background-color: var(--vp-c-bg);
box-shadow:
var(--vp-shadow-4),
-24px -24px 0 var(--vp-c-bg-soft);
display: flex;
align-items: center;
justify-content: center;
min-height: 240px;
}
.spacer {
flex-basis: 0;
}
.button {
position: relative;
border-radius: 32px;
padding: 8px 16px;
display: flex;
flex-direction: row;
gap: 12px;
font-weight: 500;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
&:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
&.button-brand {
border-color: var(--vp-button-brand-border);
color: var(--vp-button-brand-text);
background-color: var(--vp-button-brand-bg);
&:hover {
border-color: var(--vp-button-brand-hover-border);
color: var(--vp-button-brand-hover-text);
background-color: var(--vp-button-brand-hover-bg);
}
}
&.button-icon {
padding: 12px;
}
&.button-square {
border-radius: 8px;
width: 100%;
}
}
.actions {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
}
.lucide {
flex-shrink: 0;
flex-grow: 0;
}
.placeholder {
display: block;
background-color: var(--vp-c-bg-soft);
border-radius: 32px;
height: 16px;
width: 100%;
&.narrow {
width: 33%;
}
}
.input-wrapper {
position: relative;
width: 100%;
.lucide {
position: absolute;
inset-inline-start: 12px;
inset-block: 50%;
translate: 0 -50%;
color: var(--vp-c-text-1);
opacity: 0.7;
}
input {
padding: 12px 20px;
padding-inline-start: 48px;
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
display: block;
width: 100%;
border: 2px solid transparent;
transition:
color 0.25s,
border-color 0.25s,
background-color 0.25s;
&:hover {
border-color: var(--vp-c-border);
}
&:focus {
border-color: var(--vp-c-brand);
}
}
&:focus-within {
.lucide {
opacity: 1;
}
}
}
.badge {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 8px;
background-color: transparent;
overflow: hidden;
position: relative;
color: var(--badge-color);
&:before {
content: ' ';
inset: 0;
position: absolute;
background-color: var(--badge-color);
opacity: 0.1;
}
&.badge-indigo {
--badge-color: var(--vp-c-indigo-2);
}
&.badge-green {
--badge-color: var(--vp-c-green-2);
}
&.badge-red {
--badge-color: var(--vp-c-brand);
}
}
.badge-notification {
background-color: var(--vp-c-brand);
color: var(--vp-button-brand-text);
border-radius: 32px;
padding: 0 8px;
min-width: 24px;
min-height: 24px;
}
.button-icon {
.badge-notification {
position: absolute;
top: 0;
right: 0;
translate: 33% -33%;
}
}
.row {
display: flex;
flex-direction: row;
gap: 12px;
align-items: center;
width: 100%;
}
.icon-counter {
display: flex;
flex-direction: row;
gap: 6px;
color: var(--vp-c-text-2);
align-items: center;
}
.column {
display: flex;
flex-direction: column;
gap: 12px;
align-items: start;
width: 100%;
}
.align-items-center {
align-items: center;
}
.justify-content-center {
justify-content: center;
}
.ms-auto {
margin-inline-start: auto;
}
</style>

View File

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

View File

@@ -2,31 +2,19 @@
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue'; import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
import type { IconEntity } from '../../types'; import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core'; import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import { useRoute } from 'vitepress';
import IconGrid from './IconGrid.vue'; import IconGrid from './IconGrid.vue';
import InputSearch from '../base/InputSearch.vue'; import InputSearch from '../base/InputSearch.vue';
import useSearch from '../../composables/useSearch'; import useSearch from '../../composables/useSearch';
import useSearchInput from '../../composables/useSearchInput'; import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
import StickyBar from './StickyBar.vue'; import StickyBar from './StickyBar.vue';
import useFetchTags from '../../composables/useFetchTags'; import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories'; import useFetchCategories from '../../composables/useFetchCategories';
import chunkArray from '../../utils/chunkArray'; import chunkArray from '../../utils/chunkArray';
import CarbonAdOverlay from './CarbonAdOverlay.vue'; import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
const ICON_SIZE = 56; const ICON_SIZE = 56;
const ICON_GRID_GAP = 8; 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 props = defineProps<{ const props = defineProps<{
icons: IconEntity[]; icons: IconEntity[];
}>(); }>();
@@ -37,10 +25,10 @@ const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories(); const { execute: fetchCategories, data: categories } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null); const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl); const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => { 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(() => { const mappedIcons = computed(() => {
@@ -61,38 +49,30 @@ const mappedIcons = computed(() => {
}); });
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput(); const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [ const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 }, { name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 }, { name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 }, { name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 }, { name: 'categories', weight: 1 },
]); ]);
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const chunkedIcons = computed(() => { const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value); return chunkArray(searchResults.value, columnSize.value);
}); });
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(chunkedIcons, { const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
chunkedIcons,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP, itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10, overscan: 10
}); },
)
onMounted(() => { onMounted(() => {
containerProps.ref.value = document.documentElement; 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) { function setActiveIconName(name: string) {
activeIconName.value = name; activeIconName.value = name;
@@ -112,8 +92,8 @@ const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue')); const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
watch(searchQueryDebounced, () => { watch(searchQueryDebounced, () => {
scrollTo(0); scrollTo(0)
}); })
function handleCloseDrawer() { function handleCloseDrawer() {
setActiveIconName(''); setActiveIconName('');
@@ -123,38 +103,22 @@ function handleCloseDrawer() {
</script> </script>
<template> <template>
<div <div ref="overviewEl" class="overview-container">
ref="overviewEl"
class="overview-container"
>
<StickyBar> <StickyBar>
<InputSearch <InputSearch
:placeholder="`Search ${icons.length} icons ...`" :placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery" v-model="searchQuery"
ref="searchInput" ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper" class="input-wrapper"
@focus="onFocusSearchInput" @focus="onFocusSearchInput"
/> />
</StickyBar> </StickyBar>
<NoResults <NoResults
v-if="searchPlaceholder.isNoResults" v-if="list.length === 0"
:searchQuery="searchPlaceholder.query" :searchQuery="searchQuery"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''" @clear="searchQuery = ''"
/> />
<IconGrid <div v-bind="wrapperProps" class="icon">
v-else-if="list.length === 0"
overlayMode
:icons="searchResults.slice(0, initialGridItems)"
:activeIcon="activeIconName"
@setActiveIcon="setActiveIconName"
/>
<div
v-bind="wrapperProps"
class="icon"
v-else
>
<IconGrid <IconGrid
v-for="{ index, data: icons } in list" v-for="{ index, data: icons } in list"
:key="index" :key="index"
@@ -186,4 +150,8 @@ function handleCloseDrawer() {
.input-wrapper { .input-wrapper {
width: 100%; width: 100%;
} }
.overview-container {
padding-bottom: 288px;
}
</style> </style>

View File

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

View File

@@ -1,20 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import type { IconEntity } from '../../types'; import type { IconEntity } from '../../types'
import IconGrid from './IconGrid.vue'; import IconGrid from './IconGrid.vue'
defineProps<{ defineProps<{
icons: IconEntity[]; icons: IconEntity[]
}>(); }>()
</script> </script>
<template> <template>
<section class="related-icons"> <section class="related-icons">
<h2 class="title">More icons like this</h2> <h2 class="title">
<IconGrid :icons="icons" /> Related Icons
</h2>
<IconGrid
:icons="icons"
/>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.title {
margin-bottom: 24px;
font-size: 19px;
font-weight: 500;
}
.related-icons { .related-icons {
margin-bottom: 32px; margin-bottom: 32px;
} }

View File

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

View File

@@ -1,27 +1,21 @@
import packageDataList from '../../../data/packageData.json'; import packageData from '../../../data/packageData.json';
import thirdPartyPackages from '../../../data/packageData.thirdParty.json'; import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
import fetchPackages from '../../../lib/fetchPackages'; import fetchPackages from '../../../lib/fetchPackages';
export default { export default {
async load() { async load() {
const packageJsonList = await fetchPackages(); const packages = await fetchPackages();
return { return {
packages: packageJsonList packages: packages
.filter((pJson) => pJson?.name != null && pJson.name in packageDataList) .filter((p) => p?.name != null && p.name in packageData)
.map((pJson) => { .map((pData) => ({
const packageData = packageDataList[pJson.name]; ...pData,
return { ...packageData[pData.name],
...pJson, documentation: `/guide/packages/${pData.name}`,
...packageData, source: `https://github.com/lucide-icons/lucide/tree/main/packages/${pData.name}`,
documentation: `/guide/packages/${packageData.docsAlias ?? pJson.name}`, icon: `/framework-logos/${packageData[pData.name].icon}.svg`,
source: `https://github.com/lucide-icons/lucide/tree/main/packages/${packageData.packageDirname ?? pJson.name}`, }))
icon: `/framework-logos/${packageData.icon}.svg`,
iconDark: Boolean(packageData.iconDark)
? `/framework-logos/${packageData.iconDark}.svg`
: null,
};
})
.sort((a, b) => a.order - b.order), .sort((a, b) => a.order - b.order),
thirdPartyPackages, thirdPartyPackages,
}; };

View File

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

View File

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

View File

@@ -6,9 +6,6 @@ export interface IconMetaData {
categories: string[]; categories: string[];
contributors: string[]; contributors: string[];
aliases?: string[]; aliases?: string[];
deprecated?: boolean;
deprecationReason?: string;
toBeRemovedInVersion?: string;
} }
export type ExternalLibs = 'lab'; export type ExternalLibs = 'lab';
@@ -36,19 +33,12 @@ interface Shield {
export interface PackageItem { export interface PackageItem {
name: string; name: string;
// set when the package's directory
// name under the `packages/` directory
// is diffrent from the package name
packageDirname?: string;
description: string; description: string;
icon: string; icon: string;
iconDark: string; iconDark: string;
shields: Shield[]; shields: Shield[];
source: string; source: string;
documentation: string; documentation: string;
// set when the docs page name is
// diffrent from the package name
docsAlias?: string;
order?: number; order?: number;
private?: boolean; private?: boolean;
flutter?: object; flutter?: object;

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