mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-16 23:17:42 +01:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fb4b0b161 | ||
|
|
9ec40ae506 | ||
|
|
890514de6c | ||
|
|
9463b2e445 | ||
|
|
0221022e24 | ||
|
|
751f7cb1e3 | ||
|
|
4c90b84236 | ||
|
|
01be733532 | ||
|
|
4ffa38e013 | ||
|
|
0a38dccdae | ||
|
|
3391b5b717 | ||
|
|
5c145f4e72 | ||
|
|
74429a8e92 | ||
|
|
1e99499dc8 | ||
|
|
724a43e3f0 | ||
|
|
fb79059fca | ||
|
|
9a54838538 | ||
|
|
c7438c0c10 | ||
|
|
667ae303f8 | ||
|
|
dc1d17181f | ||
|
|
26abdb904d | ||
|
|
cf0264745e | ||
|
|
510a5ec3b9 | ||
|
|
e1bc19be4e | ||
|
|
062894e113 | ||
|
|
eaa99b35f6 | ||
|
|
7c22ccfab4 | ||
|
|
e0e171db81 | ||
|
|
3b11552e5e | ||
|
|
d8b455b614 | ||
|
|
79eecc89f6 | ||
|
|
b943430a08 | ||
|
|
cebb0ee84b | ||
|
|
210c56807e | ||
|
|
c2a8f31176 | ||
|
|
a3f70d5b8b | ||
|
|
c97c6ed9e4 | ||
|
|
e9d69c6948 | ||
|
|
270c935cd4 | ||
|
|
b7377d21eb | ||
|
|
7ccf155ad4 | ||
|
|
c696d1e907 | ||
|
|
a8cf24e75a | ||
|
|
4e2773bd2d | ||
|
|
7c8f898893 | ||
|
|
c3951d36c7 | ||
|
|
f958310d91 | ||
|
|
d00cbebcc4 | ||
|
|
3619dfa7f2 | ||
|
|
c844bc668f | ||
|
|
32dec05e0b | ||
|
|
5394d7fceb | ||
|
|
8dea4c7b7f | ||
|
|
b8a595275d | ||
|
|
a7b3ecde63 | ||
|
|
8bcfc225ce | ||
|
|
1d608db223 | ||
|
|
c1cb5d9bc2 | ||
|
|
66f51474cd | ||
|
|
af0b16bc10 | ||
|
|
46d6c3c119 | ||
|
|
d3b8510602 | ||
|
|
97e0f30627 | ||
|
|
24676ddc8e | ||
|
|
92689a3328 | ||
|
|
b578269ecb | ||
|
|
d041a2b02a | ||
|
|
0253326b17 | ||
|
|
275c2cbc69 | ||
|
|
4bd6a273db | ||
|
|
34155d48e7 | ||
|
|
82db590192 | ||
|
|
70be55b78f | ||
|
|
7163aeaa6b | ||
|
|
99cd76bb35 | ||
|
|
f5fb1ec263 | ||
|
|
6916aebee4 | ||
|
|
65d213264f | ||
|
|
ee77147aff | ||
|
|
3b7b74fe86 | ||
|
|
3a2f052ce9 | ||
|
|
cf34d61971 | ||
|
|
2814a63b8f | ||
|
|
4bcab462dc | ||
|
|
6c93bb97c7 | ||
|
|
3c1993c463 | ||
|
|
7a57c306c3 | ||
|
|
32637199f5 | ||
|
|
e490bc35b8 | ||
|
|
496058cc15 | ||
|
|
4ee46673af | ||
|
|
5a46f4b87c | ||
|
|
875e8a2d06 | ||
|
|
e006a171c1 | ||
|
|
606706e8e0 | ||
|
|
ffc03ea1f6 | ||
|
|
b2e685262b | ||
|
|
5bfc736b61 | ||
|
|
2ebf99f591 | ||
|
|
7a17a2f343 | ||
|
|
4b5d343791 | ||
|
|
b19b01d323 | ||
|
|
d2dc5bf75f | ||
|
|
9b93200567 | ||
|
|
a878596572 | ||
|
|
9d50c05937 | ||
|
|
6196c261d3 | ||
|
|
85cec0dea1 | ||
|
|
07039b7619 | ||
|
|
cf05bd766f | ||
|
|
f05855d1d1 | ||
|
|
6f39d3743a | ||
|
|
7ed206af4a | ||
|
|
95daa7c313 | ||
|
|
17ecb92946 | ||
|
|
9ef9921f04 | ||
|
|
ac08bb92c1 | ||
|
|
53109037ec | ||
|
|
66de90d63e | ||
|
|
f3c7e44a3d | ||
|
|
3823993c39 | ||
|
|
36c53f956a | ||
|
|
58c652908a | ||
|
|
f4d887339e | ||
|
|
bde11234ea | ||
|
|
3449097f77 | ||
|
|
aec41eae39 | ||
|
|
3da3cbc63f | ||
|
|
3fc3122054 | ||
|
|
871de752e7 | ||
|
|
25d7b55459 | ||
|
|
4d8a8091b6 | ||
|
|
a17c1aafbd | ||
|
|
d1d6eec36e | ||
|
|
abec311bc9 | ||
|
|
3df9be04a8 | ||
|
|
016c9d1fac | ||
|
|
17f9509f71 | ||
|
|
b6c7434e92 | ||
|
|
47aa3c2664 | ||
|
|
e50b03f316 | ||
|
|
0065b5952b | ||
|
|
b35b586eda | ||
|
|
8b57fab71b | ||
|
|
badd34374d | ||
|
|
902431199c | ||
|
|
bdbb4834b0 | ||
|
|
07fc4da6fa | ||
|
|
e1815242cf | ||
|
|
d104ad5c8a | ||
|
|
69989c5ae5 | ||
|
|
9e996ef63c | ||
|
|
6ec9cc3dcf | ||
|
|
01fa96ced3 | ||
|
|
481b27cc49 | ||
|
|
c5df7e73c6 | ||
|
|
428088436d | ||
|
|
eec2c97595 | ||
|
|
f0529b9ef7 | ||
|
|
0c216b41c5 | ||
|
|
ac892e5476 | ||
|
|
38f62a571c | ||
|
|
507750d0a7 | ||
|
|
33a0ed9539 | ||
|
|
37cb860ebe | ||
|
|
bd74ac880e | ||
|
|
e81b76f445 | ||
|
|
c50c0e435b | ||
|
|
afd2db296c | ||
|
|
175b2cd483 | ||
|
|
716c5baea0 | ||
|
|
890474889a | ||
|
|
e596cd2bad | ||
|
|
d4641a4641 | ||
|
|
3e3409cee2 | ||
|
|
477f2b2aff | ||
|
|
000ff56278 | ||
|
|
7117220943 | ||
|
|
f820da257d | ||
|
|
de629f0fcc | ||
|
|
8f7e9b3cde | ||
|
|
4e79f147cf | ||
|
|
3482cd0949 | ||
|
|
701f2a1a41 | ||
|
|
79f5c6e584 | ||
|
|
02fddd3aac | ||
|
|
7816ed88f6 | ||
|
|
9c2d57b0dc | ||
|
|
6d9a0c3d63 | ||
|
|
6f647c58bf | ||
|
|
41375d5b05 | ||
|
|
11ce2b6ff3 | ||
|
|
cc7881e759 |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
open_collective: lucide-icons
|
||||
8
.github/actions/build-and-test.yml
vendored
8
.github/actions/build-and-test.yml
vendored
@@ -24,15 +24,15 @@ runs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-lucide-preact-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-lucide-preact-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter lucide-preact
|
||||
|
||||
8
.github/actions/check-icons.yml
vendored
8
.github/actions/check-icons.yml
vendored
@@ -24,15 +24,15 @@ runs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-lucide-preact-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-lucide-preact-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter .
|
||||
|
||||
62
.github/labeler.yml
vendored
Normal file
62
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# For changed dependencies
|
||||
📦 dependencies:
|
||||
- pnpm-lock.yaml
|
||||
|
||||
# For changes in documentation
|
||||
📖 documentation:
|
||||
- docs/*.md
|
||||
- docs/**/*.md
|
||||
|
||||
# For changes in the site, but not markdown files
|
||||
🌍 site:
|
||||
- 'docs/**'
|
||||
|
||||
# For changes in the metadata
|
||||
🫧 metadata:
|
||||
- 'icons/*.json'
|
||||
- categories/*
|
||||
|
||||
# For changes or added icons
|
||||
🎨 icon:
|
||||
- 'icons/*.svg'
|
||||
|
||||
# For changes in the lucide package
|
||||
🧳 lucide package:
|
||||
- 'packages/lucide/*'
|
||||
|
||||
# For changes in the lucide React package
|
||||
⚛️ react package:
|
||||
- 'packages/lucide-react/*'
|
||||
|
||||
# For changes in the lucide React Native package
|
||||
⚛️ react native package:
|
||||
- 'packages/lucide-react-native/*'
|
||||
|
||||
# For changes in the lucide vue packages
|
||||
💎 vue package:
|
||||
- 'packages/lucide-vue/*'
|
||||
- 'packages/lucide-vue-next/*'
|
||||
|
||||
# For changes in the lucide angular package
|
||||
🅰️ angular package:
|
||||
- 'packages/lucide-angular/*'
|
||||
|
||||
# For changes in the lucide preact package
|
||||
⚛️ preact package:
|
||||
- 'packages/lucide-preact/*'
|
||||
|
||||
# For changes in the lucide svelte package
|
||||
🧣 svelte package:
|
||||
- 'packages/lucide-svelte/*'
|
||||
|
||||
# For changes in the lucide solid package
|
||||
🪝 solid package:
|
||||
- 'packages/lucide-solid/*'
|
||||
|
||||
# For changes in the lucide static package
|
||||
🪨 static package:
|
||||
- 'packages/lucide-static/*'
|
||||
|
||||
# For changes in the lucide flutter package
|
||||
🏹 flutter package:
|
||||
- 'packages/lucide-flutter/*'
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -10,8 +10,9 @@ on:
|
||||
jobs:
|
||||
create-release:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -29,12 +30,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
@@ -47,7 +48,7 @@ jobs:
|
||||
|
||||
- name: Get latest tag
|
||||
id: latest-tag
|
||||
run: echo "::set-output name=LATEST_TAG::$(git describe --tags `git rev-list --tags --max-count=1`)"
|
||||
run: echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
@@ -57,17 +58,18 @@ jobs:
|
||||
|
||||
- name: Create new version
|
||||
id: new-version
|
||||
run: echo "::set-output name=NEW_VERSION::$(.github/workflows/version-up.sh --minor)"
|
||||
run: echo "NEW_VERSION=$(.github/workflows/version-up.sh --minor)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create change log
|
||||
id: change-log
|
||||
run: |
|
||||
CHANGE_LOG=$(pnpm run generate:changelog --old-tag=${{ steps.latest-tag.outputs.LATEST_TAG }})
|
||||
CHANGE_LOG="${CHANGE_LOG//'%'/'%25'}"
|
||||
CHANGE_LOG="${CHANGE_LOG//$'\n'/'%0A'}"
|
||||
CHANGE_LOG="${CHANGE_LOG//$'\r'/'%0D'}"
|
||||
CHANGE_LOG=$(tail -n +5 <<< $CHANGE_LOG)
|
||||
echo $CHANGE_LOG
|
||||
echo "::set-output name=CHANGE_LOG::$CHANGE_LOG"
|
||||
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||||
echo "CHANGE_LOG<<$EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CHANGE_LOG" >> $GITHUB_OUTPUT
|
||||
echo "$EOF" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_API_KEY: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -76,11 +78,17 @@ jobs:
|
||||
echo '${{ steps.new-version.outputs.NEW_VERSION }}'
|
||||
echo '${{ steps.change-log.outputs.CHANGE_LOG }}'
|
||||
|
||||
- name: Release
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CREATE_RELEASE_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
name: New icons ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
body: ${{ steps.change-log.outputs.CHANGE_LOG }}
|
||||
|
||||
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 }}
|
||||
|
||||
12
.github/workflows/labeler.yml
vendored
Normal file
12
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
4
.github/workflows/lucide-angular.yml
vendored
4
.github/workflows/lucide-angular.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
8
.github/workflows/lucide-font.yml
vendored
8
.github/workflows/lucide-font.yml
vendored
@@ -26,15 +26,15 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-lucide-font-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-lucide-font-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter outline-svg
|
||||
|
||||
4
.github/workflows/lucide-preact.yml
vendored
4
.github/workflows/lucide-preact.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-react-native.yml
vendored
4
.github/workflows/lucide-react-native.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-react.yml
vendored
4
.github/workflows/lucide-react.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-solid.yml
vendored
4
.github/workflows/lucide-solid.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-static.yml
vendored
4
.github/workflows/lucide-static.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-svelte.yml
vendored
4
.github/workflows/lucide-svelte.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-vue-next.yml
vendored
4
.github/workflows/lucide-vue-next.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide-vue.yml
vendored
4
.github/workflows/lucide-vue.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
4
.github/workflows/lucide.yml
vendored
4
.github/workflows/lucide.yml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
89
.github/workflows/pull-request.yml
vendored
89
.github/workflows/pull-request.yml
vendored
@@ -22,8 +22,78 @@ jobs:
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: icons/*.svg
|
||||
- name: Generate comment
|
||||
id: generate-comment
|
||||
- name: Generate cohesion check random
|
||||
id: generate-cohesion-check-random
|
||||
run: |
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
for file in $(printf "%s\\n" icons/*.svg | shuf | head -n$(awk -F' ' '{print NF}' <<< '${{ steps.changed-files.outputs.all_changed_files }}')); do
|
||||
cat "$file" | # get file content
|
||||
tr '\n' ' ' | # remove line breaks
|
||||
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
|
||||
base64 -w 0 | # encode svg
|
||||
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/2/&.svg\"/> |"
|
||||
done | tr '\n' ' ' >> $GITHUB_OUTPUT
|
||||
echo >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
- name: Generate cohesion check squares
|
||||
id: generate-cohesion-check-squares
|
||||
run: |
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
for file in $(printf "%s\\n" icons/*square*.svg | shuf | head -n$(awk -F' ' '{print NF}' <<< '${{ steps.changed-files.outputs.all_changed_files }}')); do
|
||||
cat "$file" | # get file content
|
||||
tr '\n' ' ' | # remove line breaks
|
||||
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
|
||||
base64 -w 0 | # encode svg
|
||||
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/2/&.svg\"/> |"
|
||||
done | tr '\n' ' ' >> $GITHUB_OUTPUT
|
||||
echo >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
- name: Generate 1px stroke-width
|
||||
id: generate-1px-stroke-width
|
||||
run: |
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
|
||||
cat "$file" | # get file content
|
||||
tr '\n' ' ' | # remove line breaks
|
||||
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
|
||||
base64 -w 0 | # encode svg
|
||||
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/1/&.svg\"/> |"
|
||||
done | tr '\n' ' ' >> $GITHUB_OUTPUT
|
||||
echo >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
- name: Generate 2px stroke-width
|
||||
id: generate-2px-stroke-width
|
||||
run: |
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
|
||||
cat "$file" | # get file content
|
||||
tr '\n' ' ' | # remove line breaks
|
||||
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
|
||||
base64 -w 0 | # encode svg
|
||||
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/2/&.svg\"/> |"
|
||||
done | tr '\n' ' ' >> $GITHUB_OUTPUT
|
||||
echo >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
- name: Generate 3px stroke-width
|
||||
id: generate-3px-stroke-width
|
||||
run: |
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
|
||||
cat "$file" | # get file content
|
||||
tr '\n' ' ' | # remove line breaks
|
||||
sed -e 's/<svg[^>]*>/<svg>/g' | # remove attributes from svg element
|
||||
base64 -w 0 | # encode svg
|
||||
sed "s|.*|<img title=\"$file\" alt=\"$file\" src=\"https://lucide.dev/api/gh-icon/stroke-width/3/&.svg\"/> |"
|
||||
done | tr '\n' ' ' >> $GITHUB_OUTPUT
|
||||
echo >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
- name: Generate X-rays
|
||||
id: generate-x-rays
|
||||
run: |
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
@@ -50,8 +120,21 @@ jobs:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
### Added or changed icons
|
||||
${{ steps.generate-2px-stroke-width.outputs.body }}<br/>
|
||||
<details>
|
||||
<summary>Preview cohesion</summary>
|
||||
${{ steps.generate-cohesion-check-squares.outputs.body }}<br/>
|
||||
${{ steps.generate-2px-stroke-width.outputs.body }}<br/>
|
||||
${{ steps.generate-cohesion-check-random.outputs.body }}<br/>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Preview stroke widths</summary>
|
||||
${{ steps.generate-1px-stroke-width.outputs.body }}<br/>
|
||||
${{ steps.generate-2px-stroke-width.outputs.body }}<br/>
|
||||
${{ steps.generate-3px-stroke-width.outputs.body }}<br/>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Icon X-rays</summary>
|
||||
${{ steps.generate-comment.outputs.body }}
|
||||
${{ steps.generate-x-rays.outputs.body }}
|
||||
</details>
|
||||
edit-mode: replace
|
||||
|
||||
640
.github/workflows/release.yml
vendored
640
.github/workflows/release.yml
vendored
@@ -5,6 +5,13 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
description: Version
|
||||
type: string
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -12,8 +19,8 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
pre-build:
|
||||
if: github.repository == 'lucide-icons/lucide' && contains('["locness3","ericfennis", "johnletey", "karsa-mistmere"]', github.actor)
|
||||
pre-release:
|
||||
if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere"]', github.actor)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
VERSION: ${{ steps.get_version.outputs.VERSION }}
|
||||
@@ -23,19 +30,32 @@ jobs:
|
||||
id: get_version
|
||||
run: |
|
||||
echo $VERSION_REF
|
||||
echo ::set-output name=VERSION::${VERSION_REF/refs\/tags\/\v}
|
||||
echo "VERSION=${VERSION_REF/refs\/tags\/\v}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
VERSION_REF: ${{ github.event.inputs.version || github.ref }}
|
||||
VERSION_REF: ${{ inputs.version || github.event.inputs.version || github.ref }}
|
||||
|
||||
lucide:
|
||||
release:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
needs: pre-release
|
||||
strategy:
|
||||
matrix:
|
||||
package: [
|
||||
'lucide',
|
||||
'lucide-react',
|
||||
'lucide-react-native',
|
||||
'lucide-vue',
|
||||
'lucide-vue-next',
|
||||
'lucide-angular',
|
||||
'lucide-preact',
|
||||
'lucide-solid',
|
||||
'lucide-svelte',
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
@@ -47,484 +67,38 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
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
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ inputs.NPM_TOKEN || secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set new version
|
||||
run: pnpm --filter lucide version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
run: pnpm --filter ${{ matrix.package }} version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide build
|
||||
run: pnpm --filter ${{ matrix.package }} build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide test
|
||||
run: pnpm --filter ${{ matrix.package }} test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-package-json
|
||||
path: packages/lucide/package.json
|
||||
|
||||
lucide-react:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set package.json version lucide
|
||||
run: pnpm --filter lucide-react version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-react build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-react publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-react-package-json
|
||||
path: packages/lucide-react/package.json
|
||||
|
||||
lucide-react-native:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set package.json version lucide
|
||||
run: pnpm --filter lucide-react-native version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-react-native build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react-native test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-react-native publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-react-native-package-json
|
||||
path: packages/lucide-react-native/package.json
|
||||
|
||||
lucide-vue:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set new version
|
||||
run: pnpm --filter lucide-vue version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-vue build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-vue test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-vue publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-vue-package-json
|
||||
path: packages/lucide-vue/package.json
|
||||
|
||||
lucide-vue-next:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set new version
|
||||
run: pnpm --filter lucide-vue-next version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-vue-next build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-vue-next test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-vue-next publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-vue-next-package-json
|
||||
path: packages/lucide-vue-next/package.json
|
||||
|
||||
lucide-angular:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set package.json version lucide
|
||||
run: pnpm --filter lucide-angular version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-angular build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-angular test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-angular publish --no-git-checks --ignore-scripts
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-angular-package-json
|
||||
path: packages/lucide-angular/package.json
|
||||
|
||||
lucide-preact:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set package.json version lucide
|
||||
run: pnpm --filter lucide-preact version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-preact build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-preact test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-preact publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-preact-package-json
|
||||
path: packages/lucide-preact/package.json
|
||||
|
||||
lucide-solid:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set package.json version lucide
|
||||
run: pnpm --filter lucide-solid version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-solid build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-solid test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-solid publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-solid-package-json
|
||||
path: packages/lucide-solid/package.json
|
||||
|
||||
lucide-svelte:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.0.1
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set package.json version lucide
|
||||
run: pnpm --filter lucide-svelte version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-svelte build
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-svelte test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-svelte publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-svelte-package-json
|
||||
path: packages/lucide-svelte/package.json
|
||||
run: pnpm --filter ${{ matrix.package }} publish --no-git-checks --ignore-scripts
|
||||
|
||||
lucide-static:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [pre-build, lucide-font]
|
||||
needs: [pre-release, lucide-font]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v2
|
||||
@@ -543,24 +117,24 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
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
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set Auth Token
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Set new version
|
||||
run: pnpm --filter lucide-static version --new-version ${{ needs.pre-build.outputs.VERSION }} --no-git-tag-version
|
||||
run: pnpm --filter lucide-static version --new-version ${{ needs.pre-release.outputs.VERSION }} --no-git-tag-version
|
||||
|
||||
- name: Move Font
|
||||
run: cp -r lucide-font packages/lucide-static/font
|
||||
@@ -571,16 +145,10 @@ jobs:
|
||||
- name: Publish
|
||||
run: pnpm --filter lucide-static publish --no-git-checks
|
||||
|
||||
- name: Upload package.json
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-static-package-json
|
||||
path: packages/lucide-static/package.json
|
||||
|
||||
lucide-font:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: pre-build
|
||||
needs: pre-release
|
||||
container: ericfennis/lucide-font:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -598,12 +166,12 @@ jobs:
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
@@ -626,130 +194,26 @@ jobs:
|
||||
name: lucide-font
|
||||
path: lucide-font
|
||||
|
||||
lucide-flutter:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [pre-build, lucide-font]
|
||||
container:
|
||||
image: cirrusci/flutter:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.pub-cache
|
||||
key: ${{ runner.os }}-pub-${{ hashFiles('~/.pub-cache') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pub-
|
||||
|
||||
- name: Setup credentials
|
||||
run: |
|
||||
mkdir -p ~/.pub-cache
|
||||
cat <<EOF > ~/.pub-cache/credentials.json
|
||||
{
|
||||
"accessToken": "${{ secrets.GOOGLE_OAUTH_ACCESS_TOKEN }}",
|
||||
"refreshToken": "${{ secrets.GOOGLE_OAUTH_REFRESH_TOKEN }}",
|
||||
"idToken": "${{ secrets.GOOGLE_OAUTH_ID_TOKEN }}",
|
||||
"tokenEndpoint":"https://accounts.google.com/o/oauth2/token",
|
||||
"scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ],
|
||||
"expiration": 1629835569218
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Get packages
|
||||
run: flutter pub get
|
||||
working-directory: packages/lucide-flutter
|
||||
|
||||
- name: List lucide-font folder
|
||||
run: ls lucide-font
|
||||
|
||||
- name: Copy assets from lucide-font directory
|
||||
run: |
|
||||
mkdir packages/lucide-flutter/assets
|
||||
cp lucide-font/lucide.ttf packages/lucide-flutter/assets/lucide.ttf
|
||||
cp lucide-font/lucide-preview.html packages/lucide-flutter/assets/lucide-preview.html
|
||||
|
||||
- name: Generate exports file
|
||||
run: |
|
||||
dart tool/generate_fonts.dart assets/lucide-preview.html
|
||||
flutter format .
|
||||
working-directory: packages/lucide-flutter
|
||||
|
||||
- name: Test
|
||||
run: flutter test
|
||||
working-directory: packages/lucide-flutter
|
||||
|
||||
- name: Copy License
|
||||
run: cp ../../LICENSE ./LICENSE
|
||||
working-directory: packages/lucide-flutter
|
||||
|
||||
- name: Update yaml
|
||||
run: sed -E 's/(version:)[^\n]*/\1 ${{ needs.pre-build.outputs.VERSION }}/;' pubspec.yaml > pubspec && mv pubspec pubspec.yaml
|
||||
working-directory: packages/lucide-flutter
|
||||
|
||||
- name: Flutter publish
|
||||
run: flutter pub publish -f
|
||||
working-directory: packages/lucide-flutter
|
||||
|
||||
- name: Upload pubspec.yaml
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lucide-flutter-pubspec-yaml
|
||||
path: packages/lucide-flutter/pubspec.yaml
|
||||
|
||||
post-release:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
[
|
||||
pre-build,
|
||||
lucide,
|
||||
lucide-react,
|
||||
lucide-react-native,
|
||||
lucide-vue,
|
||||
lucide-vue-next,
|
||||
lucide-angular,
|
||||
lucide-svelte,
|
||||
lucide-preact,
|
||||
lucide-flutter,
|
||||
lucide-font,
|
||||
]
|
||||
needs: [
|
||||
pre-release,
|
||||
lucide-font,
|
||||
]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v2
|
||||
|
||||
- name: Commit package files
|
||||
run: |
|
||||
mv lucide-package-json/package.json packages/lucide/package.json
|
||||
mv lucide-react-package-json/package.json packages/lucide-react/package.json
|
||||
mv lucide-react-native-package-json/package.json packages/lucide-react-native/package.json
|
||||
mv lucide-vue-package-json/package.json packages/lucide-vue/package.json
|
||||
mv lucide-preact-package-json/package.json packages/lucide-preact/package.json
|
||||
mv lucide-svelte-package-json/package.json packages/lucide-svelte/package.json
|
||||
mv lucide-vue-next-package-json/package.json packages/lucide-vue-next/package.json
|
||||
mv lucide-angular-package-json/package.json packages/lucide-angular/package.json
|
||||
mv lucide-flutter-pubspec-yaml/pubspec.yaml packages/lucide-flutter/pubspec.yaml
|
||||
|
||||
- name: Commit package.jsons
|
||||
run: |
|
||||
git add packages/*/package.json packages/lucide-flutter/pubspec.yaml
|
||||
git -c user.name="Lucide Bot" -c user.email="lucide-bot@users.noreply.github.com" \
|
||||
commit -m ":package: Bump lucide package versions to ${{ needs.pre-build.outputs.VERSION }}" --no-verify --quiet
|
||||
git remote set-url --push origin https://lucide-bot:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY.git
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Zip font and icons
|
||||
run: |
|
||||
zip -r lucide-font-${{ needs.pre-build.outputs.VERSION }}.zip lucide-font
|
||||
zip -r lucide-icons-${{ needs.pre-build.outputs.VERSION }}.zip icons
|
||||
zip -r lucide-font-${{ needs.pre-release.outputs.VERSION }}.zip lucide-font
|
||||
zip -r lucide-icons-${{ needs.pre-release.outputs.VERSION }}.zip icons
|
||||
|
||||
- name: Release zip and fonts
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CREATE_RELEASE_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ needs.pre-build.outputs.VERSION }}
|
||||
tag_name: ${{ needs.pre-release.outputs.VERSION }}
|
||||
files: |
|
||||
lucide-font-${{ needs.pre-build.outputs.VERSION }}.zip
|
||||
lucide-icons-${{ needs.pre-build.outputs.VERSION }}.zip
|
||||
lucide-font-${{ needs.pre-release.outputs.VERSION }}.zip
|
||||
lucide-icons-${{ needs.pre-release.outputs.VERSION }}.zip
|
||||
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -17,6 +17,24 @@ packages/**/src/icons/*.js
|
||||
packages/**/src/icons/*.ts
|
||||
packages/**/src/icons/*.tsx
|
||||
packages/**/src/aliases.ts
|
||||
packages/**/src/dynamicIconImports.ts
|
||||
packages/**/dynamicIconImports.js
|
||||
packages/**/dynamicIconImports.d.ts
|
||||
packages/**/dynamicIconImports.js.map
|
||||
packages/**/LICENSE
|
||||
categories.json
|
||||
tags.json
|
||||
.vercel
|
||||
|
||||
# docs
|
||||
docs/.vitepress/cache
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/.temp
|
||||
docs/.vitepress/data/iconNodes
|
||||
docs/.vitepress/data/iconMetaData.ts
|
||||
docs/.vitepress/data/releaseMetaData.json
|
||||
docs/.vitepress/data/releaseMetaData
|
||||
docs/.vitepress/data/iconDetails
|
||||
docs/.vitepress/data/relatedIcons.json
|
||||
docs/.vercel
|
||||
docs/.nitro
|
||||
|
||||
95
.vscode/svg.code-snippets
vendored
Normal file
95
.vscode/svg.code-snippets
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"Lucide SVG": {
|
||||
"scope": "xml",
|
||||
"description": "Base SVG with Lucide attributes.",
|
||||
"prefix": [
|
||||
"svg",
|
||||
"lucide"
|
||||
],
|
||||
"body": [
|
||||
"<svg",
|
||||
" xmlns=\"http://www.w3.org/2000/svg\"",
|
||||
" width=\"24\"",
|
||||
" height=\"24\"",
|
||||
" viewBox=\"0 0 24 24\"",
|
||||
" fill=\"none\"",
|
||||
" stroke=\"currentColor\"",
|
||||
" stroke-width=\"2\"",
|
||||
" stroke-linecap=\"round\"",
|
||||
" stroke-linejoin=\"round\"",
|
||||
">",
|
||||
" $0",
|
||||
"</svg>"
|
||||
]
|
||||
},
|
||||
"Rectangle": {
|
||||
"scope": "xml",
|
||||
"description": "SVG `rect`angle, with Lucide defaults.",
|
||||
"prefix": [
|
||||
"rect",
|
||||
"<rect"
|
||||
],
|
||||
"body": "<rect width=\"${1:20}\" height=\"${2:12}\" x=\"${3:2}\" y=\"${4:6}\" rx=\"${5|2,1|}\"/>"
|
||||
},
|
||||
"Square": {
|
||||
"scope": "xml",
|
||||
"description": "SVG square `rect`angle, with Lucide defaults.",
|
||||
"prefix": [
|
||||
"square",
|
||||
"rect",
|
||||
"<rect",
|
||||
"tile"
|
||||
],
|
||||
"body": "<rect width=\"${1:18}\" height=\"$1\" x=\"${2:3}\" y=\"${3:$2}\" rx=\"${4|2,1|}\" />"
|
||||
},
|
||||
"Circle": {
|
||||
"scope": "xml",
|
||||
"description": "SVG `circle`, with Lucide defaults.",
|
||||
"prefix": [
|
||||
"circle",
|
||||
"<circle"
|
||||
],
|
||||
"body": "<circle cx=\"${2:12}\" cy=\"${3:$2}\" r=\"${1|10,2,.5|}\" />"
|
||||
},
|
||||
"Ellipse": {
|
||||
"scope": "xml",
|
||||
"description": "SVG `ellipse`.",
|
||||
"prefix": [
|
||||
"ellipse",
|
||||
"<ellipse"
|
||||
],
|
||||
"body": "<ellipse cx=\"${3:12}\" cy=\"${4:$3}\" rx=\"${1:10}\" ry=\"${2:$1}\" />"
|
||||
},
|
||||
"Path": {
|
||||
"scope": "xml",
|
||||
"description": "SVG custom `path`.",
|
||||
"prefix": [
|
||||
"path",
|
||||
"<path",
|
||||
"polyline",
|
||||
"<polyline",
|
||||
"polygon",
|
||||
"<polygon"
|
||||
],
|
||||
"body": "<path d=\"${1|M,m|}$0\" />"
|
||||
},
|
||||
"Line": {
|
||||
"scope": "xml",
|
||||
"description": "SVG `path`, preffered to `line` in Lucide.",
|
||||
"prefix": [
|
||||
"line",
|
||||
"<line",
|
||||
"minus"
|
||||
],
|
||||
"body": "<path d=\"M${3:5} ${4:12}${1|h,v|}${2:14}\" />"
|
||||
},
|
||||
"Dot": {
|
||||
"scope": "xml",
|
||||
"description": "SVG small dot, within the Lucide guidelines.",
|
||||
"prefix": [
|
||||
"dot",
|
||||
"."
|
||||
],
|
||||
"body": "<path d=\"M ${1:12} ${2:$1}h.01\" />"
|
||||
}
|
||||
}
|
||||
@@ -25,19 +25,19 @@ Guidelines for pull requests:
|
||||
|
||||
Please make sure you follow the icon guidelines, that should be followed to keep quality and consistency when making icons for Lucide.
|
||||
|
||||
Read it here: [ICON_GUIDELINES](/docs/icon-design-guide.md).
|
||||
Read it here: [ICON_GUIDELINES](https://lucide.dev/docs/icon-design-guide).
|
||||
|
||||
### Editor guides
|
||||
|
||||
Here you can find instructions on how to implement the guidelines with different vector graphics editors:
|
||||
|
||||
#### [Adobe Illustrator Guide](/docs/illustrator-guide.md)
|
||||
#### [Adobe Illustrator Guide](https://lucide.dev/docs/illustrator-guide)
|
||||
|
||||
You can also [download an Adobe Illustrator template](/docs/templates/illustrator-template.ai).
|
||||
You can also [download an Adobe Illustrator template](https://lucide.dev/templates/illustrator-template.ai).
|
||||
|
||||
#### [Inkscape Guide](/docs/inkscape-guide.md)
|
||||
#### [Inkscape Guide](https://lucide.dev/docs/inkscape-guide)
|
||||
|
||||
#### [Figma Guide](/docs/figma-guide.md)
|
||||
#### [Figma Guide](https://lucide.dev/docs/figma-guide)
|
||||
|
||||
### Submitting Multiple Icons
|
||||
|
||||
@@ -70,7 +70,7 @@ pnpm install # Install dependencies, including the workspace packages
|
||||
|
||||
### Packages -> PNPM Workspaces
|
||||
|
||||
To distribute different packages we use PNPM workspaces. Before you start make sure you are familiar with this concept. The concept of working in workspaces is created by Yarn, they have a well written introduction: [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces).
|
||||
To distribute different packages we use PNPM workspaces. Before you start make sure you are familiar with this concept. The concept of working in workspaces is created by Yarn, they have a well written introduction: [yarn workspaces](https://classic.yarnpkg.com/lang/enhttps://lucide.dev/docs/workspaces).
|
||||
|
||||
The configured directory for workspaces is the [packages](./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`](./pnpm-workspace.yaml).
|
||||
@@ -172,11 +172,11 @@ Includes usefully scripts to automate certain jobs. Big part of the scripts is t
|
||||
|
||||
### site
|
||||
|
||||
The lucide.dev website using [Nextjs](https://nextjs.org).
|
||||
The lucide.dev website is using [vitepress](https://vitepress.dev/) to generate the static website. The markdown files are located in the docs directory.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation files are located in the [docs](./docs) directory. All these markdown files will be loaded in the build of the lucide.dev website.
|
||||
The documentation files are located in the [docs](https://github.com/lucide-icons/lucide/tree/main/docs) directory. All these markdown files will be loaded in the build of the lucide.dev website.
|
||||
|
||||
Feel free to write, adjust or add new markdown files to improve our documentation.
|
||||
|
||||
|
||||
@@ -166,8 +166,8 @@ For more details, see the [documentation](https://github.com/lucide-icons/lucide
|
||||
### Static (svg sprite, font, icons ..)
|
||||
|
||||
Assets:
|
||||
[Font Files](https://github.com/lucide-icons/lucide/releases/tag/latest)
|
||||
[SVG Files](https://github.com/lucide-icons/lucide/releases/tag/latest)
|
||||
[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
|
||||
@@ -286,5 +286,5 @@ Thank you to all the people who contributed to Lucide!
|
||||
## Sponsors
|
||||
|
||||
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
|
||||
<img src="./site/public/vercel.svg" alt="Powered by Vercel" width="200" />
|
||||
<img src="/docs/public/vercel.svg" alt="Powered by Vercel" width="200" />
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "../category.schema.json",
|
||||
"title": "Transportation",
|
||||
"icon": "train"
|
||||
"icon": "train-front"
|
||||
}
|
||||
11
docs/.vitepress/api/categories/index.get.ts
Normal file
11
docs/.vitepress/api/categories/index.get.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { eventHandler, setResponseHeader } from 'h3'
|
||||
import iconMetaData from '../../data/iconMetaData'
|
||||
|
||||
export default eventHandler((event) => {
|
||||
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
|
||||
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(iconMetaData).map(([name, { categories }]) => [ name, categories ])
|
||||
)
|
||||
})
|
||||
41
docs/.vitepress/api/gh-icon/[...data].get.ts
Normal file
41
docs/.vitepress/api/gh-icon/[...data].get.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { eventHandler, setResponseHeader, defaultContentType } from 'h3'
|
||||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import { createElement } from 'react'
|
||||
import SvgPreview from '../../lib/SvgPreview/index.tsx';
|
||||
import iconNodes from '../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-react/src/createLucideIcon'
|
||||
import Backdrop from '../../lib/SvgPreview/Backdrop.tsx';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const { params } = event.context
|
||||
|
||||
const [name, svgData] = params.data.split('/');
|
||||
const data = svgData.slice(0, -4);
|
||||
|
||||
const src = Buffer.from(data, 'base64').toString('utf8');
|
||||
|
||||
const children = []
|
||||
|
||||
if (name in iconNodes) {
|
||||
const iconNode = iconNodes[name]
|
||||
|
||||
const LucideIcon = createLucideIcon(name, iconNode)
|
||||
const svg = renderToStaticMarkup(createElement(LucideIcon))
|
||||
const backdropString = svg.replace(/<svg[^>]*>|<\/svg>/g, '');
|
||||
|
||||
children.push(createElement(Backdrop, { backdropString, src }))
|
||||
}
|
||||
|
||||
const svg = Buffer.from(
|
||||
// We can't use jsx here, is not supported here by nitro.
|
||||
renderToString(createElement(SvgPreview, {src, showGrid: true}, children)).replace(
|
||||
/>/,
|
||||
'><style>@media screen and (prefers-color-scheme: dark) { svg { stroke: #fff } }</style>'
|
||||
)
|
||||
).toString('utf8');
|
||||
|
||||
defaultContentType(event, 'image/svg+xml')
|
||||
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
|
||||
|
||||
return svg
|
||||
})
|
||||
35
docs/.vitepress/api/gh-icon/stroke-width/[...data].get.ts
Normal file
35
docs/.vitepress/api/gh-icon/stroke-width/[...data].get.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { eventHandler, setResponseHeader, defaultContentType } from 'h3'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { createElement } from 'react'
|
||||
import SvgPreview from '../../../lib/SvgPreview/index.tsx';
|
||||
import createLucideIcon, { IconNode } from 'lucide-react/src/createLucideIcon'
|
||||
import { parseSync } from 'svgson';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const { params } = event.context
|
||||
|
||||
const [strokeWidth, svgData] = params.data.split('/');
|
||||
const data = svgData.slice(0, -4);
|
||||
|
||||
const src = Buffer.from(data, 'base64').toString('utf8');
|
||||
|
||||
const Icon = createLucideIcon(
|
||||
'icon',
|
||||
parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`).children.map(
|
||||
({ name, attributes }) => [name, attributes]
|
||||
) as IconNode
|
||||
);
|
||||
|
||||
const svg = Buffer.from(
|
||||
// We can't use jsx here, is not supported here by nitro.
|
||||
renderToString(createElement(Icon, { strokeWidth })).replace(
|
||||
/>/,
|
||||
'><style>@media screen and (prefers-color-scheme: dark) { svg { stroke: #fff } }</style>'
|
||||
)
|
||||
).toString('utf8');
|
||||
|
||||
defaultContentType(event, 'image/svg+xml')
|
||||
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
|
||||
|
||||
return svg
|
||||
})
|
||||
30
docs/.vitepress/api/icon-nodes/index.get.ts
Normal file
30
docs/.vitepress/api/icon-nodes/index.get.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { eventHandler, getQuery, setResponseHeader } from 'h3'
|
||||
import iconNodes from '../../data/iconNodes'
|
||||
import { IconNodeWithKeys } from '../../theme/types'
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const query = getQuery(event)
|
||||
|
||||
const withUniqueKeys = query.withUniqueKeys === 'true'
|
||||
|
||||
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
|
||||
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
|
||||
|
||||
if (withUniqueKeys) {
|
||||
return iconNodes
|
||||
}
|
||||
|
||||
return Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
|
||||
if (withUniqueKeys) {
|
||||
return [name, iconNode]
|
||||
}
|
||||
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs}]) => {
|
||||
return [name, attrs]
|
||||
})
|
||||
|
||||
acc[name] = newIconNode
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
45
docs/.vitepress/api/icons/[iconName].get.ts
Normal file
45
docs/.vitepress/api/icons/[iconName].get.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { eventHandler, getQuery, setResponseHeader, createError } from 'h3'
|
||||
import iconNodes from '../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-react/src/createLucideIcon'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { createElement } from 'react'
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const { params } = event.context
|
||||
|
||||
const iconNode = iconNodes[params.iconName]
|
||||
|
||||
if (iconNode == null) {
|
||||
const error = createError({
|
||||
statusCode: 404,
|
||||
message: `Icon "${params.iconName}" not found`,
|
||||
})
|
||||
|
||||
return sendError(event, error)
|
||||
}
|
||||
|
||||
const width = getQuery(event).width || undefined
|
||||
const height = getQuery(event).height || undefined
|
||||
const color = getQuery(event).color || undefined
|
||||
const strokeWidth = getQuery(event).strokeWidth || undefined
|
||||
|
||||
const LucideIcon = createLucideIcon(params.iconName, iconNode)
|
||||
|
||||
const svg = Buffer.from(
|
||||
renderToString(
|
||||
createElement(LucideIcon, {
|
||||
width,
|
||||
height,
|
||||
color: color ? `#${color}` : undefined,
|
||||
strokeWidth,
|
||||
}
|
||||
))
|
||||
).toString('utf8');
|
||||
|
||||
defaultContentType(event, 'image/svg+xml')
|
||||
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
|
||||
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
|
||||
|
||||
return svg
|
||||
|
||||
})
|
||||
11
docs/.vitepress/api/tags/index.get.ts
Normal file
11
docs/.vitepress/api/tags/index.get.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { eventHandler, setResponseHeader } from 'h3'
|
||||
import iconMetaData from '../../data/iconMetaData'
|
||||
|
||||
export default eventHandler((event) => {
|
||||
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
|
||||
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(iconMetaData).map(([name, { tags }]) => [ name, tags ])
|
||||
)
|
||||
})
|
||||
3
docs/.vitepress/api/test.ts
Normal file
3
docs/.vitepress/api/test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default eventHandler(() => {
|
||||
return { nitro: 'Is Awesome! asda' }
|
||||
})
|
||||
157
docs/.vitepress/config.ts
Normal file
157
docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { createWriteStream } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { SitemapStream } from 'sitemap'
|
||||
import sidebar from './sidebar';
|
||||
import fs from 'fs';
|
||||
|
||||
const links = []
|
||||
|
||||
|
||||
const title = "Lucide";
|
||||
const socialTitle = "Lucide Icons";
|
||||
const description = "Beautiful & consistent icon toolkit made by the community."
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title,
|
||||
description,
|
||||
cleanUrls: true,
|
||||
outDir: '.vercel/output/static',
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^.*\/VPIconAlignLeft\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/overrides/VPIconAlignLeft.vue', import.meta.url)
|
||||
)
|
||||
},
|
||||
{
|
||||
find: /^.*\/VPFooter\.vue$/,
|
||||
replacement: fileURLToPath(
|
||||
new URL('./theme/components/overrides/VPFooter.vue', import.meta.url)
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
head: [
|
||||
[ 'script', {
|
||||
src: 'https://analytics.lucide.dev/js/script.js',
|
||||
'data-domain': 'lucide.dev',
|
||||
defer: ''
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:locale",
|
||||
content:"en_US"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:type",
|
||||
content:"website"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:site_name",
|
||||
content: title,
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:title",
|
||||
content: socialTitle,
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:description",
|
||||
content: description
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:url",
|
||||
content:"https://lucide.dev"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:image",
|
||||
content: "https://lucide.dev/og.png"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:image:width",
|
||||
content:"1200"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:image:height",
|
||||
content:"630"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"og:image:type",
|
||||
content:"image/png"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"twitter:card",
|
||||
content:"summary_large_image"
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"twitter:title",
|
||||
content: socialTitle,
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"twitter:description",
|
||||
content: description
|
||||
}],
|
||||
[ 'meta', {
|
||||
property:"twitter:image",
|
||||
content:"https://lucide.dev/og.png"
|
||||
}],
|
||||
],
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: {
|
||||
light: '/logo.light.svg',
|
||||
dark: '/logo.dark.svg'
|
||||
},
|
||||
nav: [
|
||||
{ text: 'Icons', link: '/icons/' },
|
||||
{ text: 'Guide', link: '/guide/' },
|
||||
{ text: 'Packages', link: '/packages' },
|
||||
{ text: 'License', link: '/license' },
|
||||
],
|
||||
sidebar,
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/lucide-icons/lucide' },
|
||||
{ icon: 'discord', link: 'https://discord.gg/EH6nSts' }
|
||||
],
|
||||
footer: {
|
||||
message: 'Released under the ISC License.',
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path'
|
||||
},
|
||||
},
|
||||
transformHtml: (_, id, { pageData }) => {
|
||||
if (/[\\/]404\.html$/.test(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (pageData.relativePath.startsWith('icons/')) {
|
||||
links.push({
|
||||
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'),
|
||||
lastmod: pageData?.params?.changedRelease?.date
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
links.push({
|
||||
url: pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'),
|
||||
lastmod: pageData.lastUpdated
|
||||
})
|
||||
},
|
||||
buildEnd: async ({ outDir }) => {
|
||||
const sitemap = new SitemapStream({
|
||||
hostname: 'https://lucide.dev/'
|
||||
})
|
||||
const writeStream = createWriteStream(resolve(outDir, 'sitemap.xml'))
|
||||
sitemap.pipe(writeStream)
|
||||
links.forEach((link) => sitemap.write(link))
|
||||
sitemap.end()
|
||||
await new Promise((r) => writeStream.on('finish', r))
|
||||
},
|
||||
})
|
||||
@@ -22,6 +22,7 @@
|
||||
"name": "hyva-lucide-icons",
|
||||
"description": "Implementation of Lucide icon's using Hyvä's svg php viewmodal to render icons for Magento 2 Hyva theme based projects.",
|
||||
"icon": "/framework-logos/hyva.svg",
|
||||
"iconDark": "/framework-logos/hyva-dark.svg",
|
||||
"shields": [
|
||||
{
|
||||
"alt": "Latest Stable Version",
|
||||
@@ -41,6 +42,7 @@
|
||||
"name": "eleventy-lucide-icons",
|
||||
"description": "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.",
|
||||
"icon": "/framework-logos/11ty.svg",
|
||||
"iconClass": "package-icon-invert",
|
||||
"shields": [
|
||||
{
|
||||
"alt": "Latest Stable Version",
|
||||
@@ -55,5 +57,24 @@
|
||||
],
|
||||
"source": "https://github.com/GrimLink/eleventy-plugin-lucide-icons",
|
||||
"documentation": "https://github.com/GrimLink/eleventy-plugin-lucide-icons/blob/main/README.md"
|
||||
},
|
||||
{
|
||||
"name": "nuxt-lucide-icons",
|
||||
"description": "Using this module, Nuxt projects can incorporate Lucide icons. It is fully configurable and supports auto imports and tree-shaking.",
|
||||
"icon": "/framework-logos/nuxt.svg",
|
||||
"shields": [
|
||||
{
|
||||
"alt": "Latest Stable Version",
|
||||
"src": "https://img.shields.io/npm/v/nuxt-lucide-icons",
|
||||
"href": "https://www.npmjs.com/package/nuxt-lucide-icons"
|
||||
},
|
||||
{
|
||||
"alt": "Total Downloads",
|
||||
"src": "https://img.shields.io/npm/dw/nuxt-lucide-icons",
|
||||
"href": "https://www.npmjs.com/package/nuxt-lucide-icons"
|
||||
}
|
||||
],
|
||||
"source": "https://github.com/swisnl/nuxt-lucide-icons",
|
||||
"documentation": "https://github.com/swisnl/nuxt-lucide-icons/blob/main/README.md"
|
||||
}
|
||||
]
|
||||
71
docs/.vitepress/lib/SvgPreview/Backdrop.tsx
Normal file
71
docs/.vitepress/lib/SvgPreview/Backdrop.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
interface BackdropProps {
|
||||
src: string
|
||||
backdropString: string
|
||||
}
|
||||
|
||||
const Backdrop = ({ src, backdropString }: BackdropProps): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<defs xmlns="http://www.w3.org/2000/svg">
|
||||
<pattern
|
||||
id="pattern"
|
||||
width=".1"
|
||||
height=".1"
|
||||
patternUnits="userSpaceOnUse"
|
||||
patternTransform="rotate(45 50 50)"
|
||||
>
|
||||
<line stroke="red" strokeWidth={0.1} y2={1} />
|
||||
<line stroke="red" strokeWidth={0.1} y2={1} />
|
||||
</pattern>
|
||||
</defs>
|
||||
<mask id="svg-preview-backdrop-mask-outline" maskUnits="userSpaceOnUse">
|
||||
<g stroke="#fff" dangerouslySetInnerHTML={{ __html: backdropString }} />
|
||||
<g dangerouslySetInnerHTML={{ __html: src }} strokeWidth={2.05} />
|
||||
</mask>
|
||||
<mask id="svg-preview-backdrop-mask-fill" maskUnits="userSpaceOnUse">
|
||||
<g stroke="#fff" dangerouslySetInnerHTML={{ __html: backdropString }} />
|
||||
<g dangerouslySetInnerHTML={{ __html: src }} strokeWidth={2.05} />
|
||||
<g strokeWidth={1.75} dangerouslySetInnerHTML={{ __html: backdropString }} />
|
||||
</mask>
|
||||
<g
|
||||
strokeWidth={2.25}
|
||||
stroke="url(#pattern)"
|
||||
mask={'url(#svg-preview-backdrop-mask-outline)'}
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="url(#pattern)"
|
||||
opacity={0.5}
|
||||
stroke="none"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="url(#pattern)"
|
||||
stroke="none"
|
||||
mask={'url(#svg-preview-backdrop-mask-fill)'}
|
||||
/>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="red"
|
||||
opacity={0.5}
|
||||
stroke="none"
|
||||
mask={'url(#svg-preview-backdrop-mask-fill)'}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Backdrop;
|
||||
@@ -173,6 +173,31 @@ const ControlPath = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Radii = ({
|
||||
paths,
|
||||
...props
|
||||
}: { paths: Path[] } & PathProps<
|
||||
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
|
||||
any
|
||||
>) => {
|
||||
return (
|
||||
<g className="svg-preview-radii-group" {...props}>
|
||||
{paths
|
||||
.filter(({ circle }) => circle)
|
||||
.map(({ c, prev, next, circle: { x, y, r } }) =>
|
||||
c.name === 'circle' ? (
|
||||
<path d={`M${x} ${y}h.01`} />
|
||||
) : (
|
||||
<>
|
||||
<path d={`M${prev.x} ${prev.y} ${x} ${y} ${next.x} ${next.y}`} />
|
||||
<circle cy={y} cx={x} r={r} />
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const SvgPreview = React.forwardRef<
|
||||
SVGSVGElement,
|
||||
{
|
||||
@@ -184,6 +209,7 @@ const SvgPreview = React.forwardRef<
|
||||
|
||||
const darkModeCss = `@media screen and (prefers-color-scheme: dark) {
|
||||
.svg-preview-grid-group,
|
||||
.svg-preview-radii-group,
|
||||
.svg-preview-shadow-mask-group,
|
||||
.svg-preview-shadow-group {
|
||||
stroke: #fff;
|
||||
@@ -223,6 +249,13 @@ const SvgPreview = React.forwardRef<
|
||||
'#52A675',
|
||||
]}
|
||||
/>
|
||||
<Radii
|
||||
paths={paths}
|
||||
strokeWidth={0.12}
|
||||
strokeDasharray="0 0.25 0.25"
|
||||
stroke="#777"
|
||||
strokeOpacity={0.3}
|
||||
/>
|
||||
<ControlPath radius={1} paths={paths} pointSize={1} stroke="#fff" strokeWidth={0.125} />
|
||||
{children}
|
||||
</svg>
|
||||
@@ -8,6 +8,7 @@ export type Path = {
|
||||
prev: Point;
|
||||
next: Point;
|
||||
isStart: boolean;
|
||||
circle: { x: number; y: number; r: number };
|
||||
c: ReturnType<typeof getCommands>[number];
|
||||
};
|
||||
|
||||
@@ -12,33 +12,66 @@ export function assert(value: unknown): asserts value {
|
||||
}
|
||||
}
|
||||
|
||||
const extractPaths = (node: INode): { d: string; name: typeof node.name }[] => {
|
||||
if (/(rect|circle|ellipse|polygon|polyline|line|path)/.test(node.name)) {
|
||||
return [{ d: toPath(node), name: node.name }];
|
||||
const convertToPathNode = (node: INode): { d: string; name: typeof node.name } => {
|
||||
if (node.name === 'path') {
|
||||
return { d: node.attributes.d, name: node.name };
|
||||
}
|
||||
if (node.name === 'circle') {
|
||||
const cx = parseFloat(node.attributes.cx);
|
||||
const cy = parseFloat(node.attributes.cy);
|
||||
const r = parseFloat(node.attributes.r);
|
||||
return {
|
||||
d: [
|
||||
`M ${cx} ${cy - r}`,
|
||||
`a ${r} ${r} 0 0 1 ${r} ${r}`,
|
||||
`a ${r} ${r} 0 0 1 ${0 - r} ${r}`,
|
||||
`a ${r} ${r} 0 0 1 ${0 - r} ${0 - r}`,
|
||||
`a ${r} ${r} 0 0 1 ${r} ${0 - r}`,
|
||||
].join(''),
|
||||
name: node.name,
|
||||
};
|
||||
}
|
||||
return { d: toPath(node).replace(/z$/i, ''), name: node.name };
|
||||
};
|
||||
|
||||
const extractNodes = (node: INode): INode[] => {
|
||||
if (['rect', 'circle', 'ellipse', 'polygon', 'polyline', 'line', 'path'].includes(node.name)) {
|
||||
return [node];
|
||||
} else if (node.children && Array.isArray(node.children)) {
|
||||
return node.children.flatMap(extractPaths);
|
||||
return node.children.flatMap(extractNodes);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getNodes = (src: string) =>
|
||||
extractNodes(parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`));
|
||||
|
||||
export const getCommands = (src: string) =>
|
||||
extractPaths(parseSync(src)).flatMap(({ d, name }, idx) =>
|
||||
new SVGPathData(d).toAbs().commands.map((c) => ({ ...c, id: idx, name }))
|
||||
);
|
||||
getNodes(src)
|
||||
.map(convertToPathNode)
|
||||
.flatMap(({ d, name }, idx) =>
|
||||
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name }))
|
||||
);
|
||||
|
||||
export const getPaths = (src: string) => {
|
||||
const commands = getCommands(src.includes('<svg') ? src : `<svg>${src}</svg>`);
|
||||
const paths: Path[] = [];
|
||||
let prev: Point | undefined = undefined;
|
||||
let start: Point | undefined = undefined;
|
||||
const addPath = (c: (typeof commands)[number], next: Point, d?: string) => {
|
||||
const addPath = (
|
||||
c: typeof commands[number],
|
||||
next: Point,
|
||||
d?: string,
|
||||
circle?: Path['circle']
|
||||
) => {
|
||||
assert(prev);
|
||||
paths.push({
|
||||
c,
|
||||
d: d || `M${prev.x} ${prev.y}L${next.x} ${next.y}`,
|
||||
d: d || `M ${prev.x} ${prev.y} L ${next.x} ${next.y}`,
|
||||
prev,
|
||||
next,
|
||||
circle,
|
||||
isStart: start === prev,
|
||||
});
|
||||
prev = next;
|
||||
@@ -77,7 +110,7 @@ export const getPaths = (src: string) => {
|
||||
}
|
||||
case SVGPathData.CURVE_TO: {
|
||||
assert(prev);
|
||||
addPath(c, c, `M ${prev.x},${prev.y} ${encodeSVGPath(c)}`);
|
||||
addPath(c, c, `M ${prev.x} ${prev.y} ${encodeSVGPath(c)}`);
|
||||
break;
|
||||
}
|
||||
case SVGPathData.SMOOTH_CURVE_TO: {
|
||||
@@ -86,16 +119,16 @@ export const getPaths = (src: string) => {
|
||||
const reflectedCp1 = {
|
||||
x:
|
||||
previousCommand &&
|
||||
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
|
||||
previousCommand.type === SVGPathData.CURVE_TO)
|
||||
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
|
||||
previousCommand.type === SVGPathData.CURVE_TO)
|
||||
? previousCommand.relative
|
||||
? previousCommand.x2 - previousCommand.x
|
||||
: previousCommand.x2 - prev.x
|
||||
: 0,
|
||||
y:
|
||||
previousCommand &&
|
||||
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
|
||||
previousCommand.type === SVGPathData.CURVE_TO)
|
||||
(previousCommand.type === SVGPathData.SMOOTH_CURVE_TO ||
|
||||
previousCommand.type === SVGPathData.CURVE_TO)
|
||||
? previousCommand.relative
|
||||
? previousCommand.y2 - previousCommand.y
|
||||
: previousCommand.y2 - prev.y
|
||||
@@ -104,7 +137,7 @@ export const getPaths = (src: string) => {
|
||||
addPath(
|
||||
c,
|
||||
c,
|
||||
`M ${prev.x},${prev.y} ${encodeSVGPath({
|
||||
`M ${prev.x} ${prev.y} ${encodeSVGPath({
|
||||
type: SVGPathData.CURVE_TO,
|
||||
relative: false,
|
||||
x: c.x,
|
||||
@@ -119,7 +152,7 @@ export const getPaths = (src: string) => {
|
||||
}
|
||||
case SVGPathData.QUAD_TO: {
|
||||
assert(prev);
|
||||
addPath(c, c, `M ${prev.x},${prev.y} ${encodeSVGPath(c)}`);
|
||||
addPath(c, c, `M ${prev.x} ${prev.y} ${encodeSVGPath(c)}`);
|
||||
break;
|
||||
}
|
||||
case SVGPathData.SMOOTH_QUAD_TO: {
|
||||
@@ -157,7 +190,7 @@ export const getPaths = (src: string) => {
|
||||
addPath(
|
||||
c,
|
||||
c,
|
||||
`M ${prev.x},${prev.y} ${encodeSVGPath({
|
||||
`M ${prev.x} ${prev.y} ${encodeSVGPath({
|
||||
type: SVGPathData.QUAD_TO,
|
||||
relative: false,
|
||||
x: c.x,
|
||||
@@ -170,10 +203,22 @@ export const getPaths = (src: string) => {
|
||||
}
|
||||
case SVGPathData.ARC: {
|
||||
assert(prev);
|
||||
const center = arcEllipseCenter(
|
||||
prev.x,
|
||||
prev.y,
|
||||
c.rX,
|
||||
c.rY,
|
||||
c.xRot,
|
||||
c.lArcFlag,
|
||||
c.sweepFlag,
|
||||
c.x,
|
||||
c.y
|
||||
);
|
||||
addPath(
|
||||
c,
|
||||
c,
|
||||
`M ${prev.x},${prev.y} A ${c.rX} ${c.rY} ${c.xRot} ${c.lArcFlag} ${c.sweepFlag} ${c.x} ${c.y}`
|
||||
`M ${prev.x} ${prev.y} A${c.rX} ${c.rY} ${c.xRot} ${c.lArcFlag} ${c.sweepFlag} ${c.x} ${c.y}`,
|
||||
c.rX === c.rY ? { ...center, r: c.rX } : undefined
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -184,3 +229,58 @@ export const getPaths = (src: string) => {
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
export const arcEllipseCenter = (
|
||||
x1: number,
|
||||
y1: number,
|
||||
rx: number,
|
||||
ry: number,
|
||||
a: number,
|
||||
fa: number,
|
||||
fs: number,
|
||||
x2: number,
|
||||
y2: number
|
||||
) => {
|
||||
const phi = (a * Math.PI) / 180;
|
||||
|
||||
const M = [
|
||||
[Math.cos(phi), Math.sin(phi)],
|
||||
[-Math.sin(phi), Math.cos(phi)],
|
||||
];
|
||||
const V = [(x1 - x2) / 2, (y1 - y2) / 2];
|
||||
|
||||
const [x1p, y1p] = [M[0][0] * V[0] + M[0][1] * V[1], M[1][0] * V[0] + M[1][1] * V[1]];
|
||||
|
||||
rx = Math.abs(rx);
|
||||
ry = Math.abs(ry);
|
||||
|
||||
const lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
|
||||
if (lambda > 1) {
|
||||
rx = Math.sqrt(lambda) * rx;
|
||||
ry = Math.sqrt(lambda) * ry;
|
||||
}
|
||||
|
||||
const sign = fa === fs ? -1 : 1;
|
||||
|
||||
const co =
|
||||
sign *
|
||||
Math.sqrt(
|
||||
Math.max(rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p, 0) /
|
||||
(rx * rx * y1p * y1p + ry * ry * x1p * x1p)
|
||||
);
|
||||
|
||||
const V2 = [(rx * y1p) / ry, (-ry * x1p) / rx];
|
||||
const Cp = [V2[0] * co, V2[1] * co];
|
||||
|
||||
const M2 = [
|
||||
[Math.cos(phi), -Math.sin(phi)],
|
||||
[Math.sin(phi), Math.cos(phi)],
|
||||
];
|
||||
const V3 = [(x1 + x2) / 2, (y1 + y2) / 2];
|
||||
const C = [
|
||||
M2[0][0] * Cp[0] + M2[0][1] * Cp[1] + V3[0],
|
||||
M2[1][0] * Cp[0] + M2[1][1] * Cp[1] + V3[1],
|
||||
];
|
||||
|
||||
return { x: C[0], y: C[1] };
|
||||
};
|
||||
28
docs/.vitepress/lib/categories.ts
Normal file
28
docs/.vitepress/lib/categories.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import {Category, IconEntity} from "../theme/types";
|
||||
|
||||
const directory = path.join(process.cwd(), "../categories");
|
||||
|
||||
export function getAllCategoryFiles(): Category[] {
|
||||
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
|
||||
|
||||
return fileNames.map((fileName) => {
|
||||
const name = path.basename(fileName, '.json')
|
||||
const fileContent = fs.readFileSync(path.join(directory, fileName), 'utf8')
|
||||
|
||||
const parsedFileContent = JSON.parse(fileContent)
|
||||
|
||||
return {
|
||||
name,
|
||||
title: parsedFileContent.title,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function mapCategoryIconCount(categories: Category[], icons: { categories: IconEntity['categories'] }[]) {
|
||||
return categories.map((category) => ({
|
||||
...category,
|
||||
iconCount: icons.reduce((acc, curr) => (curr.categories.includes(category.name) ? ++acc : acc), 0)
|
||||
}))
|
||||
}
|
||||
218
docs/.vitepress/lib/createCodeExamples.ts
Normal file
218
docs/.vitepress/lib/createCodeExamples.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import {
|
||||
BUNDLED_LANGUAGES,
|
||||
type IThemeRegistration
|
||||
} from 'shiki'
|
||||
import {
|
||||
getHighlighter,
|
||||
} from 'shiki-processor'
|
||||
|
||||
type CodeExampleType = {
|
||||
title: string,
|
||||
lang: string,
|
||||
codes: {
|
||||
language?: string,
|
||||
code: string,
|
||||
metastring?: string,
|
||||
}[],
|
||||
}[]
|
||||
|
||||
const getIconCodes = (): CodeExampleType => {
|
||||
return [
|
||||
{
|
||||
lang: 'html',
|
||||
title: 'HTML',
|
||||
codes: [
|
||||
{
|
||||
language: 'html',
|
||||
code: `<i data-lucide-name="Name"></i>
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'tsx',
|
||||
title: 'React',
|
||||
codes: [
|
||||
{
|
||||
language: 'tsx',
|
||||
code: `import { PascalCase } from 'lucide-react';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<PascalCase />
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
`,
|
||||
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'vue',
|
||||
title: 'Vue 3',
|
||||
codes: [
|
||||
{
|
||||
language: 'vue',
|
||||
code: `<script setup>
|
||||
import { PascalCase } from 'lucide-vue-next';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PascalCase />
|
||||
</template>
|
||||
`,
|
||||
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'svelte',
|
||||
title: 'Svelte',
|
||||
codes: [
|
||||
{
|
||||
language: 'svelte',
|
||||
code: `<script>
|
||||
import { PascalCase } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
<PascalCase />
|
||||
`,
|
||||
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'preact',
|
||||
title: 'Preact',
|
||||
codes: [
|
||||
{
|
||||
language: 'tsx',
|
||||
code: `import { PascalCase } from 'lucide-preact';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<PascalCase />
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
`,
|
||||
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'solid',
|
||||
title: 'Solid',
|
||||
codes: [
|
||||
{
|
||||
language: 'tsx',
|
||||
code: `import { PascalCase } from 'lucide-solid';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<PascalCase />
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
`,
|
||||
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'angular',
|
||||
title: 'Angular',
|
||||
codes: [
|
||||
{
|
||||
language: 'tsx',
|
||||
code: `// app.module.ts
|
||||
import { LucideAngularModule, PascalCase } from 'lucide-angular';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
LucideAngularModule.pick({ PascalCase })
|
||||
],
|
||||
})
|
||||
|
||||
// app.component.html
|
||||
<lucide-icon name="Name"></lucide-icon>
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'html',
|
||||
title: 'Icon Font',
|
||||
codes: [
|
||||
{
|
||||
language: 'html',
|
||||
code: `<style>
|
||||
@import ('~lucide-static/font/Lucide.css');
|
||||
</style>
|
||||
|
||||
<div class="icon-Name"></div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
lang: 'dart',
|
||||
title: 'Flutter',
|
||||
codes: [
|
||||
{
|
||||
language: 'dart',
|
||||
code: `Icon(LucideIcons.Name);
|
||||
`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export type ThemeOptions =
|
||||
| IThemeRegistration
|
||||
| { light: IThemeRegistration; dark: IThemeRegistration }
|
||||
|
||||
const highLightCode = async (code: string, lang: string, active?: boolean) => {
|
||||
const highlighter = await getHighlighter({
|
||||
themes: ['material-theme-palenight'],
|
||||
langs: [...BUNDLED_LANGUAGES],
|
||||
processors: []
|
||||
})
|
||||
|
||||
const highlightedCode = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
// lineOptions,
|
||||
theme: 'material-theme-palenight'
|
||||
}).replace('background-color: #292D3E', '')
|
||||
|
||||
return `<div class="language-${lang} ${active ? 'active' : ''}">
|
||||
<button title="Copy Code" class="copy"></button>
|
||||
<span class="lang">${lang}</span>
|
||||
${highlightedCode}
|
||||
</div>`
|
||||
}
|
||||
|
||||
|
||||
export default async function createCodeExamples() {
|
||||
const codes = getIconCodes();
|
||||
|
||||
const codeExamplePromises = codes.map(async (codeTemplate, index) => {
|
||||
const { title, lang, codes } = codeTemplate;
|
||||
const isFirst = index === 0;
|
||||
|
||||
const code = await highLightCode(codes[0].code, codes[0].language || lang, isFirst);
|
||||
|
||||
return {
|
||||
title,
|
||||
language: codes[0].language || lang,
|
||||
code,
|
||||
};
|
||||
})
|
||||
|
||||
return Promise.all(codeExamplePromises);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { promises as fs, constants } from 'fs';
|
||||
import path from 'path';
|
||||
import yaml from 'js-yaml'
|
||||
import { PackageItem } from '../components/Package';
|
||||
import { PackageItem } from '../theme/types';
|
||||
|
||||
const fileExist = (filePath) => fs.access(filePath, constants.F_OK).then(() => true).catch(() => false)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createLucideIcon, LucideProps } from "lucide-react"
|
||||
import { IconEntity } from "src/types"
|
||||
import { createLucideIcon } from "lucide-react/src/lucide-react"
|
||||
import { type LucideProps, type IconNode } from "lucide-react/src/createLucideIcon"
|
||||
import { IconEntity } from "../theme/types"
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { IconContent } from "./generateZip";
|
||||
|
||||
const getFallbackZip = (icons: IconEntity[], params: LucideProps) => {
|
||||
return icons
|
||||
.map<IconContent>((icon) => {
|
||||
const Icon = createLucideIcon(icon.name, icon.iconNode)
|
||||
const Icon = createLucideIcon(icon.name, icon.iconNode as IconNode)
|
||||
const src = renderToStaticMarkup(<Icon {...params} />)
|
||||
return [icon.name, src]
|
||||
})
|
||||
47
docs/.vitepress/lib/icons.ts
Normal file
47
docs/.vitepress/lib/icons.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { IconNodeWithKeys } from "../theme/types";
|
||||
import iconNodes from '../data/iconNodes'
|
||||
import releaseMeta from "../data/releaseMetaData.json";
|
||||
|
||||
const DATE_OF_FORK = '2020-06-08T16:39:52+0100';
|
||||
|
||||
const directory = path.join(process.cwd(), "../icons");
|
||||
|
||||
export interface GetDataOptions {
|
||||
withChildKeys?: boolean
|
||||
}
|
||||
|
||||
export async function getData(name: string) {
|
||||
const jsonPath = path.join(directory, `${name}.json`);
|
||||
const jsonContent = fs.readFileSync(jsonPath, "utf8");
|
||||
const { tags, categories, contributors } = JSON.parse(jsonContent);
|
||||
|
||||
const iconNode = iconNodes[name]
|
||||
|
||||
const releaseData = releaseMeta?.[name] ?? {
|
||||
"createdRelease": {
|
||||
"version": "0.0.0",
|
||||
"date": DATE_OF_FORK
|
||||
},
|
||||
"changedRelease": {
|
||||
"version": "0.0.0",
|
||||
"date": DATE_OF_FORK
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
tags,
|
||||
categories,
|
||||
iconNode,
|
||||
contributors,
|
||||
...releaseData
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAllData(): Promise<{ name: string, iconNode: IconNodeWithKeys}[]> {
|
||||
const names = Object.keys(iconNodes);
|
||||
|
||||
return Promise.all(names.map((name) => getData(name)));
|
||||
}
|
||||
118
docs/.vitepress/sidebar.ts
Normal file
118
docs/.vitepress/sidebar.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { DefaultTheme, UserConfig } from "vitepress"
|
||||
|
||||
const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
|
||||
'guide':[
|
||||
{
|
||||
text: 'Introduction',
|
||||
items: [
|
||||
{ text: 'What is lucide?', link: '/guide/' },
|
||||
{ text: 'Installation', link: '/guide/installation' },
|
||||
{ text: 'Comparison', link: '/guide/comparison' }
|
||||
]
|
||||
},
|
||||
// {
|
||||
// text: 'Using Icons',
|
||||
// items: [
|
||||
// {
|
||||
// text: 'How to use icons',
|
||||
// link: 'how-to-use-icons'
|
||||
// },
|
||||
// {
|
||||
// text: 'Styling icons',
|
||||
// link: 'styling-icons'
|
||||
// },
|
||||
// {
|
||||
// text: 'Accessibility',
|
||||
// link: 'accessibility'
|
||||
// },
|
||||
// {
|
||||
// text: 'What should I use',
|
||||
// link: 'what-should-i-use'
|
||||
// },
|
||||
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
text: 'Packages',
|
||||
items: [
|
||||
{
|
||||
text: 'Lucide',
|
||||
link: '/guide/packages/lucide'
|
||||
},
|
||||
{
|
||||
text: 'Lucide React',
|
||||
link: '/guide/packages/lucide-react'
|
||||
},
|
||||
{
|
||||
text: 'Lucide React Native',
|
||||
link: '/guide/packages/lucide-react-native'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Vue',
|
||||
link: '/guide/packages/lucide-vue'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Vue Next (Vue 3)',
|
||||
link: '/guide/packages/lucide-vue-next'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Svelte',
|
||||
link: '/guide/packages/lucide-svelte'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Solid',
|
||||
link: '/guide/packages/lucide-solid'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Preact',
|
||||
link: '/guide/packages/lucide-preact'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Angular',
|
||||
link: '/guide/packages/lucide-angular'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Static',
|
||||
link: '/guide/packages/lucide-static'
|
||||
},
|
||||
{
|
||||
text: 'Lucide Flutter',
|
||||
link: '/guide/packages/lucide-flutter'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Contributing',
|
||||
items: [
|
||||
{
|
||||
text: 'Icon Design Principles',
|
||||
link: '/guide/design/icon-design-guide'
|
||||
},
|
||||
{
|
||||
text: 'Designing in Illustrator',
|
||||
link: '/guide/design/illustrator-guide'
|
||||
},
|
||||
{
|
||||
text: 'Designing in InkScape',
|
||||
link: '/guide/design/inkscape-guide'
|
||||
},
|
||||
{
|
||||
text: 'Designing in Figma',
|
||||
link: '/guide/design/figma-guide'
|
||||
},
|
||||
]
|
||||
},
|
||||
],
|
||||
'icons': [
|
||||
{ text: '', link: '/' },
|
||||
// { text: 'Categorized', link: '/icons/categorized' },
|
||||
// {
|
||||
// text: 'Categories',
|
||||
// items: [
|
||||
// ...(getAllCategoryFiles().map((category) => ({ text: category, link: `/icons/category/${category}` })))
|
||||
// ]
|
||||
// }
|
||||
],
|
||||
}
|
||||
|
||||
export default sidebar
|
||||
11
docs/.vitepress/theme/components/PageContainer.vue
Normal file
11
docs/.vitepress/theme/components/PageContainer.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 32px;
|
||||
}
|
||||
</style>
|
||||
62
docs/.vitepress/theme/components/base/Badge.vue
Normal file
62
docs/.vitepress/theme/components/base/Badge.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useRouter } from 'vitepress';
|
||||
|
||||
const { go } = useRouter()
|
||||
const props = defineProps<{
|
||||
href?: string
|
||||
}>()
|
||||
|
||||
const component = computed(() => props.href ? 'a' : 'div')
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
:href="href"
|
||||
class="badge"
|
||||
@click="props?.href ? go(href) : undefined"
|
||||
>
|
||||
<slot/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.badge, a.badge {
|
||||
display: block;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
padding: 2px 12px;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
/* width: 56px;
|
||||
height: 56px; */
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.badge[href]:hover, a.badge:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.badge[href]:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
/* color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg); */
|
||||
}
|
||||
</style>
|
||||
187
docs/.vitepress/theme/components/base/ButtonMenu.vue
Normal file
187
docs/.vitepress/theme/components/base/ButtonMenu.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
} from '@headlessui/vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { chevronUp } from '../../../data/iconNodes'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
interface Props {
|
||||
options: {
|
||||
text: string
|
||||
onClick?: () => void
|
||||
}[],
|
||||
callOptionOnClick?: boolean
|
||||
buttonClass?: string
|
||||
id: string
|
||||
popoverPosition?: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
callOptionOnClick: false,
|
||||
popoverPosition: 'bottom'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click', 'optionClick'])
|
||||
|
||||
const buttonRef = ref(null)
|
||||
|
||||
const selectedOption = useStorage(props.id, props.options[0].text)
|
||||
const selectionOptionAction = computed(() => props.options.find(option => option.text === selectedOption.value).onClick)
|
||||
|
||||
function onClick(event) {
|
||||
selectionOptionAction.value()
|
||||
|
||||
|
||||
emit('click', event)
|
||||
}
|
||||
|
||||
function onOptionClick(event, option) {
|
||||
if(!props.callOptionOnClick) {
|
||||
return
|
||||
}
|
||||
|
||||
option.onClick()
|
||||
|
||||
emit('optionClick', event)
|
||||
}
|
||||
|
||||
const ChevronUp = createLucideIcon('ChevronUp', chevronUp)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Listbox v-model="selectedOption">
|
||||
<div class="menu" >
|
||||
<div class="button-wrapper">
|
||||
<VPButton
|
||||
v-bind="$attrs"
|
||||
:text="selectedOption"
|
||||
@click="onClick"
|
||||
theme="alt"
|
||||
class="main-button"
|
||||
:class="[props.buttonClass]"
|
||||
ref="buttonRef"
|
||||
/>
|
||||
<ListboxButton
|
||||
:as="VPButton"
|
||||
:text="''"
|
||||
theme="alt"
|
||||
class="arrow-up-button"
|
||||
:class="popoverPosition"
|
||||
/>
|
||||
</div>
|
||||
<ListboxOptions class="menu-items" :class="popoverPosition">
|
||||
<ListboxOption
|
||||
as="button"
|
||||
class="menu-item"
|
||||
v-for="option in options"
|
||||
:value="option.text"
|
||||
@click="onOptionClick($event, option)"
|
||||
>
|
||||
{{ option.text }}
|
||||
</ListboxOption>
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.menu {
|
||||
position: relative;
|
||||
}
|
||||
.menu-items {
|
||||
--menu-offset: 44px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
min-width: 128px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background-color: var(--vp-c-bg);
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
transition: background-color 0.5s;
|
||||
max-height: calc(100vh - var(--vp-nav-height));
|
||||
overflow-y: auto;
|
||||
z-index: 90;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 2px 8px;
|
||||
text-align: left;
|
||||
display: block;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
white-space: nowrap;
|
||||
transition: background-color .25s,color .25s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-bg-elv-mute);
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
}
|
||||
|
||||
.main-button {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
padding-right: 12px !important;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.arrow-up-button {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
padding-left: 4px !important;
|
||||
padding-right: 8px !important;
|
||||
position: relative;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.arrow-up-button::before {
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%0A%3E%3Cpolyline points='18 15 12 9 6 15' /%3E%3C/svg%3E%0A");
|
||||
width: 20px;
|
||||
height: 28px;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dark .arrow-up-button::before {
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%0A%3E%3Cpolyline points='18 15 12 9 6 15' /%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
.menu-items.bottom {
|
||||
top: var(--menu-offset);
|
||||
}
|
||||
|
||||
.menu-items.top {
|
||||
bottom: var(--menu-offset);
|
||||
}
|
||||
|
||||
.arrow-up-button.top::before {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.arrow-up-button.bottom::before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
51
docs/.vitepress/theme/components/base/CodeGroup.vue
Normal file
51
docs/.vitepress/theme/components/base/CodeGroup.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, onMounted } from 'vue'
|
||||
const props = defineProps<{
|
||||
groups: string[] | undefined,
|
||||
groupName: string,
|
||||
}>()
|
||||
|
||||
const getSaveIdname = (name: string) => {
|
||||
return name.toLowerCase().replace(/\s/g, '-')
|
||||
}
|
||||
|
||||
const tabs = computed(() => props.groups?.map((group) => {
|
||||
return {
|
||||
id: getSaveIdname(group),
|
||||
name: group,
|
||||
}
|
||||
}))
|
||||
|
||||
const saveTabId = (id: string) => {
|
||||
localStorage.setItem(props.groupName, id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = localStorage.getItem(props.groupName)
|
||||
if (id) {
|
||||
const tab = document.getElementById(`label-tab-${id}`)
|
||||
|
||||
if (tab) {
|
||||
tab.click()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-code-group">
|
||||
<div class="tabs">
|
||||
<template v-for="(tab, index) in tabs">
|
||||
<input
|
||||
type="radio"
|
||||
:id="`tab-${tab.id}`"
|
||||
:name="`group-${groupName}`"
|
||||
:checked="index === 0"
|
||||
@change="saveTabId(tab.id)"
|
||||
>
|
||||
<label :for="`tab-${tab.id}`" :id="`label-tab-${tab.id}`">{{ tab.name }}</label>
|
||||
</template>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
93
docs/.vitepress/theme/components/base/ColorPicker.vue
Normal file
93
docs/.vitepress/theme/components/base/ColorPicker.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
id: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="color-picker">
|
||||
<div class="color-input-wrapper">
|
||||
<!-- TODO: Add currentColor div if value is currentColor -->
|
||||
<input
|
||||
type="color"
|
||||
:id="id"
|
||||
:name="id"
|
||||
class="color-input"
|
||||
v-model="value"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:id="`${id}-input`"
|
||||
:name="`${id}-input`"
|
||||
class="color-input-text"
|
||||
aria-label="Color picker input"
|
||||
v-model="value"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-input {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
}
|
||||
.color-input-wrapper {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.color-picker {
|
||||
background: var(--color-picker-bg, var(--vp-c-bg-soft));
|
||||
border-radius: 8px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.color-input-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
border-radius: 8px;
|
||||
cursor: text;
|
||||
transition: border-color 0.25s, background 0.4s ease;
|
||||
}
|
||||
|
||||
.color-picker:hover, .color-picker:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.color-input[value="currentColor"] {
|
||||
|
||||
}
|
||||
</style>
|
||||
15
docs/.vitepress/theme/components/base/EndOfPage.vue
Normal file
15
docs/.vitepress/theme/components/base/EndOfPage.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { vIntersectionObserver } from '@vueuse/components'
|
||||
|
||||
const emit = defineEmits(['end-of-page'])
|
||||
|
||||
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
|
||||
if (isIntersecting) {
|
||||
emit('end-of-page')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-intersection-observer="onIntersectionObserver" />
|
||||
</template>
|
||||
36
docs/.vitepress/theme/components/base/FakeInput.vue
Normal file
36
docs/.vitepress/theme/components/base/FakeInput.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { search } from '../../../data/iconNodes'
|
||||
|
||||
const SearchIcon = createLucideIcon('search', search)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="fake-input">
|
||||
<component :is="SearchIcon" class="search-icon"/>
|
||||
<slot/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fake-input {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 12px 16px;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
/* box-shadow: var(--vp-shadow-4), var(--vp-shadow-2); */
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.fake-input:hover, .fake-input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
</style>
|
||||
43
docs/.vitepress/theme/components/base/IconButton.vue
Normal file
43
docs/.vitepress/theme/components/base/IconButton.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<button v-bind="$attrs" class="icon-button">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.icon-button {
|
||||
display: inline-flex;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
/* width: 56px;
|
||||
height: 56px; */
|
||||
font-size: 24px;
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.icon-button.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
/* color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg); */
|
||||
}
|
||||
</style>
|
||||
81
docs/.vitepress/theme/components/base/Input.vue
Normal file
81
docs/.vitepress/theme/components/base/Input.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
type: string
|
||||
modelValue: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text'
|
||||
})
|
||||
|
||||
const input = ref()
|
||||
|
||||
defineEmits(['change', 'input', 'update:modelValue'])
|
||||
|
||||
defineExpose({
|
||||
focus: () => {
|
||||
input.value.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-wrapper">
|
||||
<slot name="icon" class="icon" />
|
||||
<input
|
||||
:type="type"
|
||||
class="input"
|
||||
:class="{'has-icon': $slots.icon}"
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.input {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.input:hover, .input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.input.has-icon {
|
||||
padding-left: 52px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.input-wrapper svg {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 12px;
|
||||
z-index: 1;
|
||||
color: var(--vp-c-text-2);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
48
docs/.vitepress/theme/components/base/InputField.vue
Normal file
48
docs/.vitepress/theme/components/base/InputField.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string
|
||||
id: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-field">
|
||||
<div class="input-label" v-if="label">
|
||||
<label :for="id" class="customize-label">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="display-value" >
|
||||
<slot name="display"/>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.customize-label {
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vt-c-text-1);
|
||||
transition: color .5s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-field:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.display-value {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
74
docs/.vitepress/theme/components/base/InputSearch.vue
Normal file
74
docs/.vitepress/theme/components/base/InputSearch.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import Input from './Input.vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { search } from '../../../data/iconNodes'
|
||||
|
||||
const SearchIcon = createLucideIcon('search', search)
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const input = ref()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
defineExpose({
|
||||
focus: () => {
|
||||
input.value.focus()
|
||||
}
|
||||
})
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Input
|
||||
ref="input"
|
||||
type="search"
|
||||
v-bind="$attrs"
|
||||
v-model="value"
|
||||
class="input-wrapper"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="SearchIcon" class="search-icon" />
|
||||
</template>
|
||||
</Input>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
justify-content: flex-start;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px 0 12px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.input:hover, .input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.input-wrapper:deep(.input) {
|
||||
/* padding: 12px 24px; */
|
||||
padding-block: 12px;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
</style>
|
||||
16
docs/.vitepress/theme/components/base/Label.vue
Normal file
16
docs/.vitepress/theme/components/base/Label.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
<template>
|
||||
<h2 class="label"><slot/></h2>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.label {
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
19
docs/.vitepress/theme/components/base/LucideIcon.vue
Normal file
19
docs/.vitepress/theme/components/base/LucideIcon.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
|
||||
export type IconNode = [elementName: string, attrs: Record<string, string>][]
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
// contributors: Contributor[];
|
||||
iconNode: IconNode;
|
||||
}>()
|
||||
|
||||
const icon = createLucideIcon(props.name, props.iconNode)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="icon" />
|
||||
</template>
|
||||
127
docs/.vitepress/theme/components/base/RangeSlider.vue
Normal file
127
docs/.vitepress/theme/components/base/RangeSlider.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
modelValue: number | string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
min: 0,
|
||||
max: 48,
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const percentage = computed<string>(() => `${((Number(props.modelValue) - props.min) / (props.max - props.min)) * 100}%`);
|
||||
// TODO: Steps must be implemented
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="slider">
|
||||
<input
|
||||
:id="id"
|
||||
type="range"
|
||||
v-bind="$attrs"
|
||||
v-bind:value="modelValue"
|
||||
v-on:input="$emit('update:modelValue', Number($event.target.value))"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
/>
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.slider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
line-height: 10px;
|
||||
height: 20px;
|
||||
--bar-color: var(--slider-bar-color, var(--vp-c-bg-soft));
|
||||
}
|
||||
|
||||
.slider:hover input{
|
||||
opacity: 1;
|
||||
}
|
||||
.slider .bar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.slider .bar:before, .slider .bar:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.slider .bar:before {
|
||||
width: v-bind(percentage);
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
background: var(--vp-c-brand);
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.slider .bar:after {
|
||||
background: var(--bar-color);
|
||||
width: calc(100% - v-bind(percentage));
|
||||
right: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.slider input {
|
||||
-webkit-appearance: none;
|
||||
width: calc(100% + 20px);
|
||||
height: 20px;
|
||||
left: -10px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
/* opacity: 0.7; */
|
||||
-webkit-transition: 0.2s;
|
||||
transition: opacity 0.2s;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
||||
/* @apply
|
||||
md:opacity-0 */
|
||||
}
|
||||
|
||||
.slider input::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider input::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
appearance: none;
|
||||
background: #FFFFFF;
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
border: none;
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
43
docs/.vitepress/theme/components/base/ResetButton.vue
Normal file
43
docs/.vitepress/theme/components/base/ResetButton.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { rotateCw } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import IconButton from "./IconButton.vue";
|
||||
|
||||
const RotateIcon = createLucideIcon('RotateIcon', rotateCw)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconButton class="reset-button">
|
||||
<RotateIcon :size="20"/>
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reset-button {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reset-button .lucide {
|
||||
transition: ease-in-out 0.1s transform;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* a rotate css animation keyframes */
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-button:active .lucide {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
77
docs/.vitepress/theme/components/base/Switch.vue
Normal file
77
docs/.vitepress/theme/components/base/Switch.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Switch } from '@headlessui/vue'
|
||||
|
||||
const enabled = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch
|
||||
v-model="enabled"
|
||||
class="switch"
|
||||
:class="{ enabled }"
|
||||
>
|
||||
<span class="thumb" />
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-input-border-color);
|
||||
background-color: var(--vp-input-switch-bg-color);
|
||||
transition: border-color 0.25s, background-color 0.4s ease;
|
||||
border-radius: 11px;
|
||||
}
|
||||
|
||||
.switch.enabled {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.switch:hover {
|
||||
border-color: var(--vp-input-hover-border-color);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
display: inline-block;
|
||||
background-color: #fff;
|
||||
transition: transform 0.25s;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
/* background-color: var(--vp-c-neutral); */
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
.switch.enabled .thumb {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
|
||||
/* ------------------- */
|
||||
.VPSwitch {
|
||||
position: relative;
|
||||
border-radius: 11px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--vp-input-border-color);
|
||||
background-color: var(--vp-input-switch-bg-color);
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
.VPSwitch:hover {
|
||||
border-color: var(--vp-input-hover-border-color);
|
||||
}
|
||||
|
||||
.dark .icon :deep(svg) {
|
||||
fill: var(--vp-c-text-1);
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
</style>
|
||||
55
docs/.vitepress/theme/components/base/Tooltip.vue
Normal file
55
docs/.vitepress/theme/components/base/Tooltip.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {useFloating, offset, shift} from '@floating-ui/vue';
|
||||
|
||||
const reference = ref(null);
|
||||
const tooltip = ref(null);
|
||||
const middleware = ref([shift(), offset(8)]);
|
||||
const { floatingStyles, update } = useFloating(reference, tooltip, {
|
||||
middleware,
|
||||
transform: false
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
}>()
|
||||
|
||||
onMounted(() => {
|
||||
update()
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span ref="reference" class="reference">
|
||||
<slot/>
|
||||
</span>
|
||||
<div ref="tooltip" class="tooltip" :style="floatingStyles">
|
||||
{{title}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reference:hover + .tooltip{
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
transform: scale(0.9);
|
||||
font-weight: 400;
|
||||
background: var(--vp-c-brand-dark);
|
||||
color: white;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
|
||||
transition-property: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
35
docs/.vitepress/theme/components/home/HomeContainer.vue
Normal file
35
docs/.vitepress/theme/components/home/HomeContainer.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
position: relative;
|
||||
padding-inline: 24px;
|
||||
margin-block: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.home-container {
|
||||
padding-inline: 48px;
|
||||
margin-block: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.home-container {
|
||||
padding-inline: 64px;
|
||||
margin-block: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.home-container {
|
||||
margin: 64px auto;
|
||||
max-width: 1152px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
docs/.vitepress/theme/components/home/HomeHeroBefore.data.ts
Normal file
16
docs/.vitepress/theme/components/home/HomeHeroBefore.data.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
async load() {
|
||||
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest').then(res => {
|
||||
if (res.ok) {
|
||||
const releaseData = res.json() as Promise<{ tag_name: string }>
|
||||
|
||||
return releaseData
|
||||
}
|
||||
return null
|
||||
}).then(res => res.tag_name)
|
||||
|
||||
return {
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
44
docs/.vitepress/theme/components/home/HomeHeroBefore.vue
Normal file
44
docs/.vitepress/theme/components/home/HomeHeroBefore.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from '../base/Badge.vue';
|
||||
import HomeContainer from './HomeContainer.vue';
|
||||
import { data } from './HomeHeroBefore.data'
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<HomeContainer class="container">
|
||||
<Badge
|
||||
:href="`https://github.com/lucide-icons/lucide/releases/tag/${data.version}`"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>{{ data.version }}</Badge>
|
||||
</HomeContainer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
margin-block: 0;;
|
||||
margin-top: 37px;
|
||||
margin-bottom: -96px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
margin-bottom: -131px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.container {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
import iconNodes from '../../../data/iconNodes'
|
||||
|
||||
const getRandomItem = <Item>(items: Item[]): Item => items[Math.floor(Math.random()*items.length)];
|
||||
|
||||
export default {
|
||||
async load() {
|
||||
const icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }))
|
||||
|
||||
const randomIcons = Array.from({ length: 200 }, () => getRandomItem(icons))
|
||||
|
||||
return {
|
||||
icons: randomIcons,
|
||||
iconsCount: icons.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
160
docs/.vitepress/theme/components/home/HomeHeroIconsCard.vue
Normal file
160
docs/.vitepress/theme/components/home/HomeHeroIconsCard.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, shallowRef, onBeforeUnmount} from 'vue';
|
||||
import { data } from './HomeHeroIconsCard.data'
|
||||
import LucideIcon from '../base/LucideIcon.vue'
|
||||
import { useRouter } from 'vitepress';
|
||||
import { random } from 'lodash-es'
|
||||
import FakeInput from '../base/FakeInput.vue'
|
||||
|
||||
const { go } = useRouter()
|
||||
const intervalTime = shallowRef()
|
||||
|
||||
const getInitialItems = () => data.icons.slice(0, 48)
|
||||
const items = ref(getInitialItems())
|
||||
let id = items.value.length + 1
|
||||
|
||||
function getRandomNewIcon() {
|
||||
const randomIndex = random(0, 200)
|
||||
const newRandomIcon = data.icons[randomIndex]
|
||||
|
||||
if (items.value.some((item) => item.name === newRandomIcon.name)) {
|
||||
return getRandomNewIcon()
|
||||
}
|
||||
|
||||
return newRandomIcon
|
||||
}
|
||||
|
||||
function insert() {
|
||||
const replaceIndex = random(0, 48)
|
||||
const newIcon = getRandomNewIcon()
|
||||
|
||||
// items.value.splice(replaceIndex, 0, newIcon);
|
||||
|
||||
items.value[replaceIndex] = newIcon
|
||||
}
|
||||
|
||||
function startInterval() {
|
||||
intervalTime.value = setInterval(() => {
|
||||
insert()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// TODO: Try maybe something else for better pref performance
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', startInterval, { once: true })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(intervalTime.value)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-wrapper">
|
||||
<div class="icons-card">
|
||||
<div class="card-grid">
|
||||
<TransitionGroup name="list" mode="out-in">
|
||||
<div
|
||||
v-for="icon in items"
|
||||
:key="icon.name"
|
||||
class="random-icon"
|
||||
>
|
||||
<LucideIcon
|
||||
v-bind="icon"
|
||||
/>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<FakeInput @click="go('/icons/?focus')" class="search-box">
|
||||
Search {{ data.iconsCount }} icons...
|
||||
</FakeInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-wrapper {
|
||||
/* padding: 0 24px; */
|
||||
margin-left: auto;
|
||||
margin-bottom: auto;
|
||||
margin-top: 48px;
|
||||
}
|
||||
.icons-card {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
height:100%;
|
||||
/* box-shadow: var(--vp-shadow-2); */
|
||||
max-height: 220px;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
/* max-height: 240px; */
|
||||
/* margin-top: 96px; */
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
|
||||
grid-template-rows: repeat(auto-fill, minmax(36px, 1fr));
|
||||
width: 100%;
|
||||
height:100%;
|
||||
max-height: 168px;
|
||||
max-width: 512px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
/* white-space: nowrap; */
|
||||
}
|
||||
|
||||
.list-enter-active {
|
||||
transition: all 0.5s cubic-bezier(.85,.85,.25,1.1);
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.01);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: -64px;
|
||||
}
|
||||
|
||||
.random-icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.search-box {
|
||||
top: unset;
|
||||
bottom: -24px;
|
||||
left: -24px;
|
||||
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.dark .search-box {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.card-wrapper {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
195
docs/.vitepress/theme/components/home/HomeIconCustomizer.vue
Normal file
195
docs/.vitepress/theme/components/home/HomeIconCustomizer.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { syncRef, useCssVar } from '@vueuse/core'
|
||||
import HomeContainer from './HomeContainer.vue'
|
||||
import RangeSlider from '../base/RangeSlider.vue'
|
||||
import InputField from '../base/InputField.vue'
|
||||
import ColorPicker from '../base/ColorPicker.vue'
|
||||
import ResetButton from '../base/ResetButton.vue'
|
||||
import HomeIconCustomizerIcons from './HomeIconCustomizerIcons.vue'
|
||||
import Switch from '../base/Switch.vue'
|
||||
|
||||
|
||||
const iconContainer = ref<HTMLElement | null>()
|
||||
const color = ref('currentColor')
|
||||
const strokeWidth = ref(2)
|
||||
const size = ref(24)
|
||||
const absoluteStrokeWidth = ref(false)
|
||||
|
||||
const colorCssVar = useCssVar(
|
||||
'--customize-color',
|
||||
iconContainer,
|
||||
{
|
||||
initialValue: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const strokeWidthCssVar = useCssVar(
|
||||
'--customize-strokeWidth',
|
||||
iconContainer,
|
||||
{
|
||||
initialValue: '2'
|
||||
}
|
||||
)
|
||||
|
||||
const sizeCssVar = useCssVar(
|
||||
'--customize-size',
|
||||
iconContainer,
|
||||
{
|
||||
initialValue: '24'
|
||||
}
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar)
|
||||
syncRef(size, sizeCssVar)
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
strokeWidth.value = 2
|
||||
size.value = 24
|
||||
}
|
||||
|
||||
watch(absoluteStrokeWidth, (enabled) => {
|
||||
iconContainer.value?.classList.toggle('absolute-stroke-width', enabled)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HomeContainer>
|
||||
<div class="card">
|
||||
<div class="card-column">
|
||||
<h2 class="title">
|
||||
Style as you please
|
||||
<ResetButton @click="resetStyle"></ResetButton>
|
||||
</h2>
|
||||
<p class="copy">
|
||||
Lucide has a lot of customization options to match the icons with your UI.
|
||||
</p>
|
||||
|
||||
<div class="customizer">
|
||||
<InputField
|
||||
id="icon-color"
|
||||
label="Color"
|
||||
class="color-picker-field"
|
||||
>
|
||||
<template #display>
|
||||
<ColorPicker v-model="color" id="icon-color" />
|
||||
</template>
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="stroke-width"
|
||||
label="Stroke width"
|
||||
>
|
||||
<template #display>
|
||||
<span class="customize-label">{{ strokeWidth }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="stroke-width"
|
||||
name="stroke-width"
|
||||
v-model="strokeWidth"
|
||||
:min="1"
|
||||
:max="3"
|
||||
:step="0.25"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="size"
|
||||
label="Size"
|
||||
>
|
||||
<template #display>
|
||||
<span class="customize-label">{{ size }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="size"
|
||||
name="size"
|
||||
v-model="size"
|
||||
:min="16"
|
||||
:max="48"
|
||||
:step="4"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="absolute-stroke-width"
|
||||
label="Absolute Stroke width"
|
||||
>
|
||||
<template #display>
|
||||
<Switch
|
||||
id="absolute-stroke-width"
|
||||
name="absolute-stroke-width"
|
||||
v-model="absoluteStrokeWidth"
|
||||
/>
|
||||
</template>
|
||||
</InputField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="icons-container card-column" ref="iconContainer">
|
||||
<HomeIconCustomizerIcons />
|
||||
</div>
|
||||
</div>
|
||||
</HomeContainer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
display: block;
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 24px;
|
||||
--slider-bar-color: var(--vp-c-bg-soft-down);
|
||||
--color-picker-bg: var(--vp-c-bg-soft-down);
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.copy {
|
||||
padding-top: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.customizer {
|
||||
margin-top: 32px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: 8fr 10fr;
|
||||
}
|
||||
/*
|
||||
.card-column {
|
||||
flex: 1;
|
||||
} */
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.card {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-field:deep(.display-value) {
|
||||
width: 138px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { data } from './HomeHeroIconsCard.data'
|
||||
import LucideIcon from '../base/LucideIcon.vue'
|
||||
import { vIntersectionObserver } from '@vueuse/components'
|
||||
|
||||
const getInitialItems = () => data.icons.slice(0, 64)
|
||||
const items = ref(getInitialItems())
|
||||
const showIcons = ref(false)
|
||||
|
||||
// Added intersection observer to improve performance
|
||||
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
|
||||
if (isIntersecting) {
|
||||
showIcons.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-grid" v-intersection-observer="onIntersectionObserver">
|
||||
<template v-if="showIcons">
|
||||
<div
|
||||
v-for="icon in items"
|
||||
class="icon-grid-item"
|
||||
>
|
||||
<LucideIcon v-bind="icon" class="lucide-icon"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(68px, 1fr));
|
||||
grid-template-rows: repeat(auto-fill, minmax(68px, 1fr));
|
||||
width: 100%;
|
||||
height:100%;
|
||||
max-height: 360px;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
border: 8px solid var(--vp-c-bg);
|
||||
position: relative;
|
||||
top: 48px;
|
||||
right: 0;
|
||||
box-shadow: var(--vp-shadow-4);
|
||||
}
|
||||
|
||||
.icon-grid-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.icon-grid {
|
||||
top: 0;
|
||||
right: -48px;
|
||||
}
|
||||
}
|
||||
|
||||
.lucide-icon {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2);
|
||||
width: calc(var(--customize-size, 24) * 1px);
|
||||
height: calc(var(--customize-size, 24) * 1px);
|
||||
}
|
||||
|
||||
.icons-container.absolute-stroke-width .lucide-icon {
|
||||
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
export default {
|
||||
async load() {
|
||||
return {
|
||||
packages: [
|
||||
{
|
||||
name: 'lucide',
|
||||
logo: '/framework-logos/js.svg',
|
||||
label: 'Lucide documentation for JavaScript',
|
||||
},
|
||||
{
|
||||
name: 'lucide-react',
|
||||
logo: '/framework-logos/react.svg',
|
||||
label: 'Lucide documentation for React',
|
||||
},
|
||||
{
|
||||
name: 'lucide-vue-next',
|
||||
logo: '/framework-logos/vue.svg',
|
||||
label: 'Lucide documentation for Vue 3',
|
||||
},
|
||||
{
|
||||
name: 'lucide-svelte',
|
||||
logo: '/framework-logos/svelte.svg',
|
||||
label: 'Lucide documentation for Svelte',
|
||||
},
|
||||
{
|
||||
name: 'lucide-preact',
|
||||
logo: '/framework-logos/preact.svg',
|
||||
label: 'Lucide documentation for Preact',
|
||||
},
|
||||
{
|
||||
name: 'lucide-solid',
|
||||
logo: '/framework-logos/solid.svg',
|
||||
label: 'Lucide documentation for Solid',
|
||||
},
|
||||
{
|
||||
name: 'lucide-angular',
|
||||
logo: '/framework-logos/angular.svg',
|
||||
label: 'Lucide documentation for Angular',
|
||||
},
|
||||
{
|
||||
name: 'lucide-react-native',
|
||||
logo: '/framework-logos/react-native.svg',
|
||||
label: 'Lucide documentation for React Native',
|
||||
},
|
||||
{
|
||||
name: 'lucide-flutter',
|
||||
logo: '/framework-logos/flutter.svg',
|
||||
label: 'Lucide documentation for Flutter',
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import HomeContainer from './HomeContainer.vue'
|
||||
import { useRouter } from 'vitepress';
|
||||
import { data } from './HomePackagesSection.data'
|
||||
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
|
||||
|
||||
const { go } = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HomeContainer>
|
||||
<h2 class="section-title">Available For:</h2>
|
||||
<div class="packages-list">
|
||||
<a
|
||||
v-for="{ name, logo } in data.packages"
|
||||
:href="`/guide/packages/${name}`"
|
||||
class="package-logo"
|
||||
:aria-label="`Read more about: ${name} package`"
|
||||
@click.prevent="go(`/guide/packages/${name}`)"
|
||||
>
|
||||
<img
|
||||
:src="logo"
|
||||
height="36"
|
||||
width="36"
|
||||
loading="lazy"
|
||||
:alt="`${name} logo`"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="more-button-wrapper">
|
||||
<VPButton text="And more" href="/packages" theme="alt" class="more-button"/>
|
||||
</div>
|
||||
</HomeContainer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-title {
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.packages-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 -0.5rem;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.more-button-wrapper {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.package-logo {
|
||||
transition: opacity ease-in .15s;
|
||||
}
|
||||
|
||||
.package-logo:hover {
|
||||
opacity: .6;
|
||||
}
|
||||
</style>
|
||||
16
docs/.vitepress/theme/components/icons/CategoryList.data.ts
Normal file
16
docs/.vitepress/theme/components/icons/CategoryList.data.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getAllData } from '../../../lib/icons';
|
||||
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
|
||||
import iconsMetaData from '../../../data/iconMetaData'
|
||||
|
||||
|
||||
export default {
|
||||
async load() {
|
||||
let categories = getAllCategoryFiles()
|
||||
|
||||
categories = mapCategoryIconCount(categories, Object.values(iconsMetaData))
|
||||
|
||||
return {
|
||||
categories,
|
||||
}
|
||||
}
|
||||
}
|
||||
99
docs/.vitepress/theme/components/icons/CategoryList.vue
Normal file
99
docs/.vitepress/theme/components/icons/CategoryList.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
import { isActive } from 'vitepress/dist/client/shared'
|
||||
import { useActiveAnchor } from '../../composables/useActiveAnchor'
|
||||
import { data } from './CategoryList.data'
|
||||
import CategoryListItem from './CategoryListItem.vue'
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const categoriesIsActive = computed(() => {
|
||||
return isActive(page.value.relativePath, '/icons/categories');
|
||||
});
|
||||
|
||||
const overviewIsActive = computed(() => {
|
||||
return isActive(page.value.relativePath, '/icons/');
|
||||
});
|
||||
|
||||
const headers = computed(() => {
|
||||
const linkPrefix = page.value.relativePath.startsWith('icons/categories')
|
||||
? '' : '/icons/categories'
|
||||
|
||||
return data.categories.map(({ name, title, iconCount }) => ({
|
||||
level: 2,
|
||||
link: `${linkPrefix}#${name}`,
|
||||
title,
|
||||
iconCount
|
||||
}))
|
||||
})
|
||||
|
||||
const container = ref()
|
||||
const marker = ref()
|
||||
|
||||
useActiveAnchor(container, marker)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="category-list" ref="container">
|
||||
<VPLink class="sidebar-title" href="/icons/" :class="{ 'active': overviewIsActive } ">
|
||||
All
|
||||
</VPLink>
|
||||
<VPLink class="sidebar-title" href="/icons/categories" :class="{ 'active': categoriesIsActive } ">
|
||||
Categories
|
||||
</VPLink>
|
||||
<div class="content">
|
||||
<div class="outline-marker" ref="marker" />
|
||||
<nav aria-labelledby="doc-outline-aria-label">
|
||||
<CategoryListItem :headers="headers" :root="true" />
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-title {
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 6px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.sidebar-title:hover, .sidebar-title.active {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.content {
|
||||
margin-top: 12px;
|
||||
position: relative;
|
||||
border-left: 1px solid var(--vp-c-divider);
|
||||
padding-left: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.outline-marker {
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
left: -1px;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background-color: var(--vp-c-brand);
|
||||
transition: top 0.25s cubic-bezier(0, 1, 0.5, 1), background-color 0.5s, opacity 0.25s;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 28px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.root {
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
95
docs/.vitepress/theme/components/icons/CategoryListItem.vue
Normal file
95
docs/.vitepress/theme/components/icons/CategoryListItem.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useCategoryView } from '../../composables/useCategoryView'
|
||||
|
||||
interface Header {
|
||||
level: number
|
||||
title: string
|
||||
slug: string
|
||||
iconCount: number
|
||||
link: string
|
||||
children: Header[]
|
||||
}
|
||||
|
||||
type MenuItem = Omit<Header, 'slug' | 'children'> & {
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
headers: MenuItem[]
|
||||
root?: boolean
|
||||
}>()
|
||||
|
||||
const { selectedCategory } = useCategoryView()
|
||||
|
||||
function onClick(event: Event) {
|
||||
const target = (event.target as HTMLElement).nodeName === 'span' ? (event.target as HTMLElement).parentNode : event.target as HTMLElement
|
||||
const id = '#' + (target as HTMLAnchorElement).href!.split('#')[1]
|
||||
const decodedId = decodeURIComponent(id)
|
||||
|
||||
selectedCategory.value = decodedId.replace('#', '')
|
||||
|
||||
const heading = document.querySelector<HTMLAnchorElement>(decodedId)
|
||||
heading?.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul :class="root ? 'root' : 'nested'">
|
||||
<li v-for="{ children, link, title, iconCount } in headers">
|
||||
<a
|
||||
class="outline-link"
|
||||
:href="link"
|
||||
@click="onClick"
|
||||
:title="title"
|
||||
>
|
||||
<span>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span class="icon-count" :aria-label="`Count of icons in ${title}`">
|
||||
{{ iconCount }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.root {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nested {
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.outline-link {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
line-height: 28px;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.5s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.outline-link:hover,
|
||||
.outline-link.active {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.outline-link.nested {
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.icon-count {
|
||||
opacity: 0.5;
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
111
docs/.vitepress/theme/components/icons/CopyCodeButton.vue
Normal file
111
docs/.vitepress/theme/components/icons/CopyCodeButton.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { startCase, camelCase } from 'lodash-es'
|
||||
import ButtonMenu from '../base/ButtonMenu.vue'
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import useConfetti from '../../composables/useConfetti';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
popoverPosition?: 'top' | 'bottom'
|
||||
}>()
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext()
|
||||
const { animate, confetti } = useConfetti()
|
||||
const componentName = computed(() => {
|
||||
return startCase(camelCase(props.name)).replace(/\s/g, '')
|
||||
})
|
||||
|
||||
function copyJSX() {
|
||||
let attrs = ['']
|
||||
|
||||
if (size.value && size.value !== 24) {
|
||||
attrs.push(`size={${size.value}}`)
|
||||
}
|
||||
|
||||
if (color.value && color.value !== 'currentColor') {
|
||||
attrs.push(`color="${color.value}"`)
|
||||
}
|
||||
|
||||
if (strokeWidth.value && strokeWidth.value !== 2) {
|
||||
attrs.push(`strokeWidth={${strokeWidth.value}}`)
|
||||
}
|
||||
|
||||
if (absoluteStrokeWidth.value) {
|
||||
attrs.push(`absoluteStrokeWidth`)
|
||||
}
|
||||
|
||||
const code = `<${componentName.value}${attrs.join(' ')} />`
|
||||
|
||||
navigator.clipboard.writeText(code)
|
||||
}
|
||||
|
||||
function copyVue() {
|
||||
let attrs = ['']
|
||||
|
||||
if (size.value && size.value !== 24) {
|
||||
attrs.push(`:size="${size.value}"`)
|
||||
}
|
||||
|
||||
if (color.value && color.value !== 'currentColor') {
|
||||
attrs.push(`color="${color.value}"`)
|
||||
}
|
||||
|
||||
if (strokeWidth.value && strokeWidth.value !== 2) {
|
||||
attrs.push(`:stroke-width="${strokeWidth.value}"`)
|
||||
}
|
||||
|
||||
if (absoluteStrokeWidth.value) {
|
||||
attrs.push(`absoluteStrokeWidth`)
|
||||
}
|
||||
|
||||
const code = `<${componentName.value}${attrs.join(' ')} />`
|
||||
|
||||
navigator.clipboard.writeText(code)
|
||||
}
|
||||
|
||||
function copyAngular() {
|
||||
let attrs = ['']
|
||||
|
||||
attrs.push(`name="${props.name}"`)
|
||||
|
||||
if (size.value && size.value !== 24) {
|
||||
attrs.push(`[size]="${size.value}"`)
|
||||
}
|
||||
|
||||
if (color.value && color.value !== 'currentColor') {
|
||||
attrs.push(`color="${color.value}"`)
|
||||
}
|
||||
|
||||
if (strokeWidth.value && strokeWidth.value !== 2) {
|
||||
attrs.push(`[strokeWidth]="${strokeWidth.value}"`)
|
||||
}
|
||||
|
||||
if (absoluteStrokeWidth.value) {
|
||||
attrs.push(`[absoluteStrokeWidth]="true"`)
|
||||
}
|
||||
|
||||
const code = `<lucide-icon${attrs.join(' ')}></lucide-icon>`
|
||||
|
||||
navigator.clipboard.writeText(code)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ButtonMenu
|
||||
:buttonClass="`confetti-button ${animate ? 'animate' : ''}`"
|
||||
id="copy-code-button"
|
||||
callOptionOnClick
|
||||
@click="confetti"
|
||||
@optionClick="confetti"
|
||||
data-confetti-text="Copied!"
|
||||
:popoverPosition="popoverPosition"
|
||||
:options="[
|
||||
{ text: 'Copy JSX' , onClick: copyJSX },
|
||||
{ text: 'Copy Vue' , onClick: copyVue },
|
||||
{ text: 'Copy Svelte' , onClick: copyJSX },
|
||||
{ text: 'Copy Angular' , onClick: copyAngular },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style src="./confetti.css" />
|
||||
86
docs/.vitepress/theme/components/icons/CopySVGButton.vue
Normal file
86
docs/.vitepress/theme/components/icons/CopySVGButton.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import ButtonMenu from '../base/ButtonMenu.vue'
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import useConfetti from '../../composables/useConfetti';
|
||||
import getSVGIcon from '../../utils/getSVGIcon';
|
||||
import downloadData from '../../utils/downloadData';
|
||||
|
||||
const downloadText = 'Download!'
|
||||
const copiedText = 'Copied!'
|
||||
const confettiText = ref(copiedText)
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
popoverPosition?: 'top' | 'bottom'
|
||||
}>()
|
||||
|
||||
const { size } = useIconStyleContext()
|
||||
|
||||
const { animate, confetti } = useConfetti()
|
||||
|
||||
function copySVG() {
|
||||
confettiText.value = copiedText
|
||||
const svgString = getSVGIcon()
|
||||
|
||||
navigator.clipboard.writeText(svgString)
|
||||
|
||||
confetti()
|
||||
}
|
||||
|
||||
function copyDataUrl() {
|
||||
confettiText.value = copiedText
|
||||
const svgString = getSVGIcon()
|
||||
|
||||
// Create SVG data url
|
||||
const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`
|
||||
navigator.clipboard.writeText(dataUrl)
|
||||
|
||||
confetti()
|
||||
}
|
||||
|
||||
function downloadSVG() {
|
||||
confettiText.value = downloadText
|
||||
const svgString = getSVGIcon()
|
||||
|
||||
downloadData(`${props.name}.svg`, `data:image/svg+xml;base64,${btoa(svgString)}`)
|
||||
confetti()
|
||||
}
|
||||
|
||||
function downloadPNG() {
|
||||
confettiText.value = downloadText
|
||||
const svgString = getSVGIcon()
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size.value;
|
||||
canvas.height = size.value;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const image = new Image();
|
||||
image.src = `data:image/svg+xml;base64,${btoa(svgString)}`;
|
||||
image.onload = function() {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
downloadData(`${props.name}.png`, canvas.toDataURL('image/png'))
|
||||
confetti()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ButtonMenu
|
||||
:buttonClass="`confetti-button ${animate ? 'animate' : ''}`"
|
||||
callOptionOnClick
|
||||
id="copy-svg-button"
|
||||
:data-confetti-text="confettiText"
|
||||
:popoverPosition="popoverPosition"
|
||||
:options="[
|
||||
{ text: 'Copy SVG' , onClick: copySVG },
|
||||
{ text: 'Copy Data URL' , onClick: copyDataUrl },
|
||||
{ text: 'Download SVG' , onClick: downloadSVG },
|
||||
{ text: 'Download PNG' , onClick: downloadPNG },
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style src="./confetti.css" />
|
||||
88
docs/.vitepress/theme/components/icons/IconContributors.vue
Normal file
88
docs/.vitepress/theme/components/icons/IconContributors.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import {IconEntity} from "../../types";
|
||||
import Label from "../base/Label.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="contributors" v-if="props.icon.contributors?.length>0">
|
||||
<Label>Contributors:</Label>
|
||||
<div class="avatar-group">
|
||||
<a class="avatar"
|
||||
v-for="contributor in props.icon.contributors"
|
||||
:key="contributor"
|
||||
:href="`https://github.com/${contributor}`"
|
||||
target="_blank"
|
||||
:data-name="contributor"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<img class="avatar-image" :alt="contributor" :src="`https://github.com/${contributor}.png?size=128`" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.contributors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
/* justify-content: flex-end; */
|
||||
gap: 16px;
|
||||
}
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
}
|
||||
.avatar:not(:first-child) {
|
||||
margin-left: -24px;
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
}
|
||||
.avatar:before {
|
||||
content: attr(data-name);
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
transform: translateX(-50%) scale(0.9);
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
top: -28px;
|
||||
left: 50%;
|
||||
background: var(--vp-c-brand-dark);
|
||||
color: white;
|
||||
z-index: 10;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: cubic-bezier(0.19, 1, 0.22, 1) .2s;
|
||||
transition-property: opacity, transform;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.avatar:hover:before {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
.avatar-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--vp-c-bg-elv);
|
||||
background-color: var(--vp-c-neutral);
|
||||
}
|
||||
.avatar:hover .avatar-image {
|
||||
border: 2px solid var(--vp-c-bg-soft-mute);
|
||||
}
|
||||
|
||||
.avatar:hover {
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
56
docs/.vitepress/theme/components/icons/IconDetail.vue
Normal file
56
docs/.vitepress/theme/components/icons/IconDetail.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { onMounted, computed } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import IconPreview from './IconPreview.vue'
|
||||
|
||||
const { params } = useData()
|
||||
|
||||
onMounted(() => {
|
||||
console.log(params, 'data')
|
||||
})
|
||||
const tags = computed(() => {
|
||||
if (!params.tags) return []
|
||||
return params.tags.join(' • ')
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <PageContainer class="overview"> -->
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- </PageContainer> -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.overview {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
.preview {
|
||||
flex: 1;
|
||||
}
|
||||
.details {
|
||||
flex: 2
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.2;
|
||||
color: var(--vp-text-color);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
</style>
|
||||
74
docs/.vitepress/theme/components/icons/IconDetailName.vue
Normal file
74
docs/.vitepress/theme/components/icons/IconDetailName.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue';
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
import { copy } from '../../../data/iconNodes'
|
||||
import useConfetti from '../../composables/useConfetti';
|
||||
const { animate, confetti } = useConfetti()
|
||||
const slots = useSlots()
|
||||
|
||||
const copiedText = computed(() => slots.default?.()[0].children)
|
||||
|
||||
function copyText() {
|
||||
navigator.clipboard.writeText(copiedText.value)
|
||||
|
||||
confetti()
|
||||
}
|
||||
|
||||
const Copy = createLucideIcon('ChevronUp', copy)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1
|
||||
class="icon-name confetti-button"
|
||||
:class="{animate}"
|
||||
data-confetti-text="Copied!"
|
||||
@click="copyText"
|
||||
>
|
||||
<slot />
|
||||
<Copy :size="20" class="copy-icon"/>
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import './confetti.css';
|
||||
.icon-name {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
transition: background ease-in .15s;;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
width: auto;
|
||||
display: inline-flex;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
.icon-name:hover {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.icon-name:hover .copy-icon {
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.icon-name:before,
|
||||
.icon-name:after {
|
||||
left: unset !important;
|
||||
right: -20%;
|
||||
}
|
||||
|
||||
.icon-name:before {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
opacity: 0;
|
||||
margin-left: 12px;
|
||||
margin-top: 6px;
|
||||
transition:ease .3s opacity;
|
||||
}
|
||||
|
||||
.icon-name:hover .copy-icon:hover {
|
||||
opacity: .6;
|
||||
}
|
||||
</style>
|
||||
161
docs/.vitepress/theme/components/icons/IconDetailOverlay.vue
Normal file
161
docs/.vitepress/theme/components/icons/IconDetailOverlay.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconEntity } from '../../types'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import IconContributors from './IconContributors.vue';
|
||||
import IconPreview from './IconPreview.vue';
|
||||
import { x, expand } from '../../../data/iconNodes'
|
||||
import { useRouter } from 'vitepress';
|
||||
import IconInfo from './IconInfo.vue';
|
||||
import Badge from '../base/Badge.vue';
|
||||
import { computedAsync } from '@vueuse/core';
|
||||
|
||||
const props = defineProps<{
|
||||
iconName: string
|
||||
}>()
|
||||
|
||||
const icon = computedAsync<IconEntity | null>(async () => {
|
||||
if (props.iconName) {
|
||||
return (await import(`../../../data/iconDetails/${props.iconName}.ts`)).default as IconEntity
|
||||
}
|
||||
return null
|
||||
}, null)
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const isOpen = computed(() => !!icon.value)
|
||||
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const { go } = useRouter()
|
||||
|
||||
const CloseIcon = createLucideIcon('Close', x)
|
||||
const Expand = createLucideIcon('Expand', expand)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="drawer" appear>
|
||||
<div class="overlay-container" v-if="icon">
|
||||
<div class="overlay-panel">
|
||||
<nav class="overlay-menu">
|
||||
<Badge
|
||||
v-if="icon.createdRelease"
|
||||
class="version"
|
||||
:href="`https://github.com/lucide-icons/lucide/releases/tag/v${icon.createdRelease.version}`"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>v{{ icon.createdRelease.version }}</Badge>
|
||||
<IconButton @click="go(`/icons/${icon.name}`)">
|
||||
<component :is="Expand" />
|
||||
</IconButton>
|
||||
<IconButton @click="onClose">
|
||||
<component :is="CloseIcon" />
|
||||
</IconButton>
|
||||
</nav>
|
||||
<IconPreview
|
||||
id="previewer"
|
||||
:name="icon.name"
|
||||
:iconNode="icon.iconNode"
|
||||
customizable
|
||||
/>
|
||||
<IconInfo :icon="icon" popoverPosition="top">
|
||||
<template v-slot:footer>
|
||||
<IconContributors :icon="icon" class="contributors" />
|
||||
</template>
|
||||
</IconInfo>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overlay-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: var(--left, 0);
|
||||
right: var(--right, 0);
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.overlay-container {
|
||||
--left: var(--vp-sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.overlay-container {
|
||||
--left: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
--right: calc(((100% - (var(--vp-layout-max-width) - var(--vp-sidebar-width))) - 272px) / 2);
|
||||
}
|
||||
.overlay-panel {
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
will-change: transform;
|
||||
pointer-events: all;
|
||||
height: 288px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
box-shadow: var(--vp-shadow-5);
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
padding-left: 24px;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.icon-tags {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.overlay-menu {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer-enter-active {
|
||||
transition: all 0.2s cubic-bezier(.21,.8,.46,.9);
|
||||
}
|
||||
|
||||
.drawer-leave-active {
|
||||
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.drawer-enter-from,
|
||||
.drawer-leave-to {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.version {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.contributors {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
</style>
|
||||
48
docs/.vitepress/theme/components/icons/IconGrid.vue
Normal file
48
docs/.vitepress/theme/components/icons/IconGrid.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconEntity } from '../../types'
|
||||
import IconItem from './IconItem.vue'
|
||||
|
||||
const emit = defineEmits(['setActiveIcon'])
|
||||
|
||||
const props = defineProps<{
|
||||
icons: IconEntity[]
|
||||
activeIcon?: string
|
||||
overlayMode?: boolean
|
||||
hideIcons?: boolean
|
||||
}>()
|
||||
|
||||
function setActiveIcon(name: string) {
|
||||
emit('setActiveIcon', name)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icons">
|
||||
<div class="icon" v-for="icon in icons" :key="icon.name">
|
||||
<IconItem
|
||||
v-bind="icon"
|
||||
@setActiveIcon="setActiveIcon(icon.name)"
|
||||
:active="activeIcon === icon.name"
|
||||
customizable
|
||||
:overlayMode="overlayMode"
|
||||
:hideIcon="hideIcons"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.icons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
|
||||
/* padding: 32px 32px 96px; */
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
aspect-ratio: 1/1;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
137
docs/.vitepress/theme/components/icons/IconInfo.vue
Normal file
137
docs/.vitepress/theme/components/icons/IconInfo.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { IconEntity } from '../../types';
|
||||
import IconDetailName from './IconDetailName.vue';
|
||||
import Badge from '../base/Badge.vue';
|
||||
import CopySVGButton from './CopySVGButton.vue';
|
||||
import CopyCodeButton from './CopyCodeButton.vue';
|
||||
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
|
||||
import {useData, useRouter} from 'vitepress';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity
|
||||
popoverPosition?: 'top' | 'bottom'
|
||||
}>()
|
||||
|
||||
const { go } = useRouter()
|
||||
const { page } = useData()
|
||||
|
||||
const tags = computed(() => {
|
||||
if (!props.icon || !props?.icon?.tags) return []
|
||||
return props.icon.tags.join(' • ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-info">
|
||||
<IconDetailName class="icon-name">
|
||||
{{ icon.name }}
|
||||
</IconDetailName>
|
||||
<div class="tags-scroller" v-if="tags.length">
|
||||
<p class="icon-tags horizontal-scroller">
|
||||
{{ tags }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="group">
|
||||
<Badge
|
||||
v-for="category in icon.categories"
|
||||
class="category"
|
||||
:href="`/icons/categories#${category}`"
|
||||
>
|
||||
{{ category }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="group buttons">
|
||||
<VPButton
|
||||
v-if="!page?.relativePath?.startsWith?.(`icons/${icon.name}`)"
|
||||
:href="`/icons/${icon.name}`"
|
||||
text="See in action"
|
||||
@click="go(`/icons/${icon.name}`)"
|
||||
/>
|
||||
<CopySVGButton :name="icon.name" :popoverPosition="popoverPosition"/>
|
||||
<CopyCodeButton :name="icon.name" :popoverPosition="popoverPosition"/>
|
||||
</div>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.category {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.icon-name {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.icon-tags {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tags-scroller {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 8px 0 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 8px;
|
||||
align-items: center;
|
||||
|
||||
--gradient-background: var(--tags-gradient-background, var(--vp-c-bg-elv))
|
||||
}
|
||||
.horizontal-scroller {
|
||||
overflow-x: scroll;
|
||||
/* Hide Scrollbar */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
|
||||
scrollbar-color: currentColor transparent; /* foreground background */
|
||||
}
|
||||
.horizontal-scroller::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none
|
||||
}
|
||||
|
||||
.horizontal-scroller::-webkit-scrollbar-track {
|
||||
background: transparent
|
||||
}
|
||||
|
||||
.horizontal-scroller::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
border: none
|
||||
}
|
||||
|
||||
|
||||
.tags-scroller::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
/* Background Gradient left to right */
|
||||
background: linear-gradient(to right, rgba(255,255,255,0) 0%,var(--gradient-background) 100%);
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
162
docs/.vitepress/theme/components/icons/IconItem.vue
Normal file
162
docs/.vitepress/theme/components/icons/IconItem.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { useRouter } from 'vitepress';
|
||||
import getSVGIcon from '../../utils/getSVGIcon';
|
||||
import useConfetti from '../../composables/useConfetti';
|
||||
import Tooltip from '../base/Tooltip.vue';
|
||||
|
||||
const downloadText = 'Download!'
|
||||
const copiedText = 'Copied!'
|
||||
|
||||
export type IconNode = [elementName: string, attrs: Record<string, string>][]
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
iconNode: IconNode;
|
||||
active: boolean;
|
||||
customizable?: boolean;
|
||||
overlayMode?: boolean
|
||||
hideIcon?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['setActiveIcon'])
|
||||
|
||||
const { go } = useRouter()
|
||||
const showOverlay = useMediaQuery('(min-width: 860px)');
|
||||
const { animate, confetti, confettiText } = useConfetti()
|
||||
|
||||
|
||||
const icon = createLucideIcon(props.name, props.iconNode)
|
||||
|
||||
async function navigateToIcon(event) {
|
||||
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault()
|
||||
const svgString = getSVGIcon(event.target.firstChild, {
|
||||
class: `lucide lucide-${props.name}`,
|
||||
})
|
||||
|
||||
await navigator.clipboard.writeText(svgString)
|
||||
|
||||
confettiText.value = copiedText
|
||||
confetti()
|
||||
return
|
||||
}
|
||||
|
||||
if(props.overlayMode && showOverlay.value) {
|
||||
event.preventDefault()
|
||||
window.history.pushState({}, '', `/icons/${props.name}`)
|
||||
emit('setActiveIcon', props.name)
|
||||
}
|
||||
else {
|
||||
event.preventDefault()
|
||||
go(`/icons/${props.name}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tooltip :title="name">
|
||||
<a
|
||||
class="icon-button confetti-button vp-raw"
|
||||
@click="navigateToIcon"
|
||||
:class="{ active, animate }"
|
||||
:aria-label="name"
|
||||
:href="`/icons/${props.name}`"
|
||||
:data-confetti-text="confettiText"
|
||||
ref="ref"
|
||||
>
|
||||
<KeepAlive>
|
||||
<component
|
||||
v-if="!hideIcon"
|
||||
:is="icon"
|
||||
class="lucide-icon"
|
||||
:class="{
|
||||
customizable,
|
||||
}"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<style src="./confetti.css" />
|
||||
|
||||
<style scoped>
|
||||
.icon-button {
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
display: inline-flex;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.confetti-button:before,
|
||||
.confetti-button:after {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.confetti-button:before {
|
||||
line-height: 80px;
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.icon-button.medium {
|
||||
border-radius: 20px;
|
||||
padding: 0 20px;
|
||||
line-height: 38px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.icon-button.big {
|
||||
border-radius: 24px;
|
||||
padding: 0 24px;
|
||||
line-height: 46px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
.icon-button.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.lucide-icon {
|
||||
margin: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
.lucide-icon.customizable {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2);
|
||||
width: calc(var(--customize-size, 24) * 1px);
|
||||
height: calc(var(--customize-size, 24) * 1px);
|
||||
}
|
||||
|
||||
html.absolute-stroke-width .lucide-icon.customizable {
|
||||
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
|
||||
}
|
||||
</style>
|
||||
73
docs/.vitepress/theme/components/icons/IconPreview.vue
Normal file
73
docs/.vitepress/theme/components/icons/IconPreview.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconEntity } from '../../types'
|
||||
import { computed, ref } from 'vue'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconEntity['name']
|
||||
iconNode: IconEntity['iconNode']
|
||||
customizable?: boolean
|
||||
}>()
|
||||
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext()
|
||||
const previewIcon = ref()
|
||||
|
||||
const gridLines = computed(() => Array.from({ length:(size.value - 1) }))
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
if (!props.name || !props.iconNode) return null
|
||||
return createLucideIcon(props.name, props.iconNode)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<component
|
||||
ref="previewIcon"
|
||||
:is="iconComponent"
|
||||
:width="size"
|
||||
:height="size"
|
||||
:stroke="color"
|
||||
:stroke-width="absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth"
|
||||
/>
|
||||
<svg class="icon-grid" :viewBox="`0 0 ${size} ${size}`" fill="none" stroke-width="0.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g :key="`grid-${i}`" v-for="(_, i) in gridLines">
|
||||
<line :key="`horizontal-${i}`" :x1="0" :y1="i + 1" :x2="size" :y2="i + 1" />
|
||||
<line :key="`vertical-${i}`" :x1="i + 1" y1="0" :x2="i + 1" :y2="size" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
stroke: var(--vp-c-divider);
|
||||
}
|
||||
.icon-container {
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
position: relative;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.icon-container > :deep(svg:not(.icon-grid)) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--vp-c-neutral);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.icon-component.customizable {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
/* color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2); */
|
||||
}
|
||||
</style>
|
||||
69
docs/.vitepress/theme/components/icons/IconPreviewSmall.vue
Normal file
69
docs/.vitepress/theme/components/icons/IconPreviewSmall.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconEntity } from '../../types'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconEntity['name']
|
||||
iconNode: IconEntity['iconNode']
|
||||
customizable?: boolean
|
||||
}>()
|
||||
|
||||
const Icon = createLucideIcon(props.name, props.iconNode)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icons-small-preview">
|
||||
<div class="icon-wrapper">
|
||||
<Icon :size="48" class="lucide-icon"/>
|
||||
</div>
|
||||
<div class="icon-wrapper">
|
||||
<Icon :size="32" class="lucide-icon"/>
|
||||
</div>
|
||||
<div class="icon-wrapper">
|
||||
<Icon class="lucide-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icons-small-preview {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* align-items: center; */
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
/* margin-top: 24px; */
|
||||
}
|
||||
.icon-wrapper {
|
||||
border: 1px solid transparent;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
display: inline-flex;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-text-1);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lucide-icon {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2);
|
||||
/* Not sure if this is logical for 100% previews */
|
||||
/* width: calc(var(--customize-size, 24) * 1px);
|
||||
height: calc(var(--customize-size, 24) * 1px); */
|
||||
}
|
||||
|
||||
html.absolute-stroke-width .lucide-icon {
|
||||
stroke-width: calc(var(--customize-strokeWidth, 2) * 24 / var(--customize-size, 24));
|
||||
}
|
||||
</style>
|
||||
56
docs/.vitepress/theme/components/icons/IconsCategory.vue
Normal file
56
docs/.vitepress/theme/components/icons/IconsCategory.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Category } from '../../types';
|
||||
import IconGrid from './IconGrid.vue'
|
||||
import { vIntersectionObserver } from '@vueuse/components'
|
||||
|
||||
defineProps<{
|
||||
activeIconName: string
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
|
||||
const emit = defineEmits(['setActiveIcon'])
|
||||
|
||||
const showIcons = ref(false)
|
||||
|
||||
// Added intersection observer to improve performance
|
||||
const onIntersectionObserver: IntersectionObserverCallback = ([{ isIntersecting }]) => {
|
||||
showIcons.value = isIntersecting
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="category"
|
||||
:key="category.name"
|
||||
:id="category.name"
|
||||
v-intersection-observer="onIntersectionObserver"
|
||||
>
|
||||
<h2 class="title" >
|
||||
<a class="header-anchor" :href="`#${category.name}`" :aria-label="`Permalink to "${category.title}"`">​</a>
|
||||
{{ category.title }}
|
||||
</h2>
|
||||
<IconGrid
|
||||
:activeIcon="activeIconName"
|
||||
:icons="category.icons"
|
||||
@setActiveIcon="$event => $emit('setActiveIcon', $event)"
|
||||
overlayMode
|
||||
:hideIcons="!showIcons"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin-bottom: 24px;
|
||||
font-size: 19px;
|
||||
font-weight: 500;
|
||||
padding-top: 86px;
|
||||
/* scroll-padding-top: 240px; */
|
||||
}
|
||||
|
||||
.category {
|
||||
margin-bottom: calc(-86px + 32px);
|
||||
}
|
||||
</style>
|
||||
130
docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
Normal file
130
docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineAsyncComponent } from 'vue'
|
||||
import type { IconEntity, Category } from '../../types'
|
||||
import useSearch from '../../composables/useSearch'
|
||||
import InputSearch from '../base/InputSearch.vue'
|
||||
import useSearchInput from '../../composables/useSearchInput'
|
||||
import StickyBar from './StickyBar.vue'
|
||||
import IconsCategory from './IconsCategory.vue'
|
||||
import { useFetch } from '@vueuse/core'
|
||||
import useFetchTags from '../../composables/useFetchTags'
|
||||
import useFetchCategories from '../../composables/useFetchCategories'
|
||||
|
||||
const props = defineProps<{
|
||||
icons: IconEntity[]
|
||||
categories: Category[]
|
||||
iconCategories: Record<string, string[]>
|
||||
}>()
|
||||
|
||||
const activeIconName = ref(null)
|
||||
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
|
||||
|
||||
const isSearching = computed(() => !!searchQuery.value)
|
||||
|
||||
function setActiveIconName(name: string) {
|
||||
activeIconName.value = name
|
||||
}
|
||||
|
||||
const { execute: fetchTags, data: tags } = useFetchTags()
|
||||
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories()
|
||||
|
||||
const mappedIcons = computed(() => {
|
||||
if(tags.value == null) {
|
||||
return props.icons
|
||||
}
|
||||
return props.icons.map((icon) => {
|
||||
const iconTags = tags.value[icon.name]
|
||||
const iconCategories = categoriesMap.value?.[icon.name] ?? []
|
||||
|
||||
return {
|
||||
...icon,
|
||||
tags: iconTags,
|
||||
categories: iconCategories,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const searchResults = useSearch(searchQuery, mappedIcons, [
|
||||
{ name: 'name', weight: 2 },
|
||||
{ name: 'tags', weight: 1 },
|
||||
])
|
||||
|
||||
const categories = computed(() => {
|
||||
if( !props.categories?.length || !props.icons?.length ) return []
|
||||
|
||||
return props.categories.map(({ name, title }) => {
|
||||
const categoryIcons = props.icons.filter((icon) => {
|
||||
const iconCategories = props.iconCategories[icon.name]
|
||||
|
||||
return iconCategories?.includes(name)
|
||||
})
|
||||
|
||||
const searchedCategoryIcons = isSearching
|
||||
? categoryIcons.filter(icon => searchResults.value.some((item) => item?.name === icon?.name))
|
||||
: categoryIcons;
|
||||
|
||||
return {
|
||||
title,
|
||||
name,
|
||||
icons: searchedCategoryIcons,
|
||||
};
|
||||
})
|
||||
.filter(({ icons }) => icons.length)
|
||||
})
|
||||
|
||||
function onFocusSearchInput() {
|
||||
if (tags.value == null) {
|
||||
fetchTags()
|
||||
}
|
||||
if (categoriesMap.value == null) {
|
||||
fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const NoResults = defineAsyncComponent(() =>
|
||||
import('./NoResults.vue')
|
||||
)
|
||||
|
||||
const IconDetailOverlay = defineAsyncComponent(() =>
|
||||
import('./IconDetailOverlay.vue')
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StickyBar class="search-bar category-search">
|
||||
<InputSearch
|
||||
:placeholder="`Search ${icons.length} icons ...`"
|
||||
v-model="searchQuery"
|
||||
class="input-wrapper"
|
||||
ref="searchInput"
|
||||
@focus="onFocusSearchInput"
|
||||
/>
|
||||
</StickyBar>
|
||||
<NoResults
|
||||
v-if="categories.length === 0"
|
||||
:searchQuery="searchQuery"
|
||||
@clear="searchQuery = ''"
|
||||
/>
|
||||
<IconsCategory
|
||||
v-for="category in categories"
|
||||
:key="category.name"
|
||||
:category="category"
|
||||
:activeIconName="activeIconName"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
/>
|
||||
<IconDetailOverlay
|
||||
v-if="activeIconName != null"
|
||||
:iconName="activeIconName"
|
||||
@close="setActiveIconName('')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar.category-search {
|
||||
margin-bottom: -54px;
|
||||
}
|
||||
</style>
|
||||
154
docs/.vitepress/theme/components/icons/IconsOverview.vue
Normal file
154
docs/.vitepress/theme/components/icons/IconsOverview.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, defineAsyncComponent } from 'vue'
|
||||
import type { IconEntity } from '../../types'
|
||||
import { useMediaQuery, useOffsetPagination } from '@vueuse/core'
|
||||
import IconGrid from './IconGrid.vue'
|
||||
import InputSearch from '../base/InputSearch.vue'
|
||||
import useSearch from '../../composables/useSearch'
|
||||
import EndOfPage from '../base/EndOfPage.vue'
|
||||
import useSearchInput from '../../composables/useSearchInput'
|
||||
import StickyBar from './StickyBar.vue'
|
||||
import useFetchTags from '../../composables/useFetchTags'
|
||||
import useFetchCategories from '../../composables/useFetchCategories'
|
||||
|
||||
const props = defineProps<{
|
||||
icons: IconEntity[]
|
||||
}>()
|
||||
|
||||
const activeIconName = ref(null)
|
||||
|
||||
const isExtraLargeScreen = useMediaQuery('(min-width: 1440px)');
|
||||
const isLargeScreen = useMediaQuery('(min-width: 1280px)');
|
||||
const isMediumScreen = useMediaQuery('(min-width: 960px)');
|
||||
const isSmallScreen = useMediaQuery('(min-width: 640px)');
|
||||
|
||||
const pageSize = computed(() => {
|
||||
if(isExtraLargeScreen.value) {
|
||||
return 16 * 20;
|
||||
}
|
||||
if(isLargeScreen.value) {
|
||||
return 16 * 12;
|
||||
}
|
||||
if(isMediumScreen.value) {
|
||||
return 13 * 12;
|
||||
}
|
||||
|
||||
if(isSmallScreen.value) {
|
||||
return 10 * 10;
|
||||
}
|
||||
|
||||
return 10 * 5;
|
||||
})
|
||||
|
||||
const { execute: fetchTags, data: tags } = useFetchTags()
|
||||
const { execute: fetchCategories, data: categories } = useFetchCategories()
|
||||
|
||||
const mappedIcons = computed(() => {
|
||||
if(tags.value == null) {
|
||||
return props.icons
|
||||
}
|
||||
|
||||
return props.icons.map((icon) => {
|
||||
const iconTags = tags.value[icon.name]
|
||||
const iconCategories = categories.value?.[icon.name] ?? []
|
||||
|
||||
return {
|
||||
...icon,
|
||||
tags: iconTags,
|
||||
categories: iconCategories,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
|
||||
const searchResults = useSearch(searchQueryThrottled, mappedIcons, [
|
||||
{ name: 'name', weight: 3 },
|
||||
{ name: 'tags', weight: 2 },
|
||||
{ name: 'categories', weight: 1 },
|
||||
])
|
||||
|
||||
const { next, currentPage } = useOffsetPagination( { pageSize })
|
||||
|
||||
|
||||
const paginatedIcons = computed(() => {
|
||||
const end = pageSize.value * currentPage.value
|
||||
|
||||
return searchResults.value.slice(0, end)
|
||||
})
|
||||
|
||||
function setActiveIconName(name: string) {
|
||||
activeIconName.value = name
|
||||
}
|
||||
|
||||
watch(searchQueryThrottled, (searchString) => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
function onFocusSearchInput() {
|
||||
if (tags.value == null) {
|
||||
fetchTags()
|
||||
}
|
||||
if (categories.value == null) {
|
||||
fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const NoResults = defineAsyncComponent(() =>
|
||||
import('./NoResults.vue')
|
||||
)
|
||||
|
||||
const IconDetailOverlay = defineAsyncComponent(() =>
|
||||
import('./IconDetailOverlay.vue')
|
||||
)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StickyBar>
|
||||
<InputSearch
|
||||
:placeholder="`Search ${icons.length} icons ...`"
|
||||
v-model="searchQuery"
|
||||
ref="searchInput"
|
||||
class="input-wrapper"
|
||||
@focus="onFocusSearchInput"
|
||||
/>
|
||||
</StickyBar>
|
||||
<NoResults
|
||||
v-if="paginatedIcons.length === 0"
|
||||
:searchQuery="searchQuery"
|
||||
@clear="searchQuery = ''"
|
||||
/>
|
||||
<IconGrid
|
||||
overlayMode
|
||||
:activeIcon="activeIconName"
|
||||
:icons="paginatedIcons"
|
||||
@setActiveIcon="setActiveIconName"
|
||||
/>
|
||||
<EndOfPage @end-of-page="next" class="bottom-page"/>
|
||||
<IconDetailOverlay
|
||||
v-if="activeIconName != null"
|
||||
:iconName="activeIconName"
|
||||
@close="setActiveIconName('')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.icons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bottom-page {
|
||||
height: 288px;
|
||||
}
|
||||
</style>
|
||||
83
docs/.vitepress/theme/components/icons/NoResults.vue
Normal file
83
docs/.vitepress/theme/components/icons/NoResults.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {bird} 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'
|
||||
|
||||
defineProps<{
|
||||
searchQuery: string
|
||||
}>()
|
||||
|
||||
defineEmits(['clear'])
|
||||
|
||||
const birdIcon = ref<HTMLElement>()
|
||||
const Bird = createLucideIcon('bird', bird)
|
||||
const flip = ref(false)
|
||||
|
||||
useEventListener(document, 'mousemove', (mouseEvent) => {
|
||||
const {width, height, x, y} = birdIcon.value.getBoundingClientRect()
|
||||
|
||||
const centerX = (width / 2) + x
|
||||
|
||||
flip.value = mouseEvent.x < centerX
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="no-results">
|
||||
<Bird class="bird-icon" ref="birdIcon" :class="{ flip }" :strokeWidth="1"/>
|
||||
<h2 class="no-results-text">
|
||||
No icons found for '{{ searchQuery }}'
|
||||
</h2>
|
||||
<VPButton text="Clear your search and try again" theme="alt" @click="$emit('clear')"/>
|
||||
<span class="text-divider">or</span>
|
||||
<VPButton text="Search on Github issues"
|
||||
theme="alt"
|
||||
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.no-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bird-icon {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
color: var(--vp-c-neutral);
|
||||
opacity: 0.8;
|
||||
margin-top: 72px;
|
||||
}
|
||||
|
||||
.bird-icon.flip {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.bird-icon {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results-text {
|
||||
line-height: 40px;
|
||||
font-size: 24px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-divider {
|
||||
margin: 12px 0;
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-neutral);
|
||||
}
|
||||
</style>
|
||||
30
docs/.vitepress/theme/components/icons/RelatedIcons.vue
Normal file
30
docs/.vitepress/theme/components/icons/RelatedIcons.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { IconEntity } from '../../types'
|
||||
import IconGrid from './IconGrid.vue'
|
||||
defineProps<{
|
||||
icons: IconEntity[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="related-icons">
|
||||
<h2 class="title">
|
||||
Related Icons
|
||||
</h2>
|
||||
<IconGrid
|
||||
:icons="icons"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin-bottom: 24px;
|
||||
font-size: 19px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.related-icons {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
</style>
|
||||
169
docs/.vitepress/theme/components/icons/SidebarIconCustomizer.vue
Normal file
169
docs/.vitepress/theme/components/icons/SidebarIconCustomizer.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, type Ref, watch, computed } from 'vue'
|
||||
import { useCssVar, syncRef } from '@vueuse/core'
|
||||
import { STYLE_DEFAULTS, useIconStyleContext } from '../../composables/useIconStyle'
|
||||
import RangeSlider from '../base/RangeSlider.vue'
|
||||
import InputField from '../base/InputField.vue'
|
||||
import ColorPicker from '../base/ColorPicker.vue'
|
||||
import ResetButton from '../base/ResetButton.vue'
|
||||
import Switch from '../base/Switch.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
rootEl?: Ref<HTMLElement>
|
||||
}>()
|
||||
|
||||
const { color, strokeWidth, size, absoluteStrokeWidth } = useIconStyleContext()
|
||||
const documentRef = shallowRef<HTMLElement | undefined>(typeof document !== 'undefined' ? document?.documentElement : undefined)
|
||||
|
||||
const colorCssVar = useCssVar(
|
||||
'--customize-color',
|
||||
props.rootEl?.value ?? documentRef.value,
|
||||
{
|
||||
initialValue: `${STYLE_DEFAULTS.color}`
|
||||
}
|
||||
)
|
||||
|
||||
const strokeWidthCssVar = useCssVar(
|
||||
'--customize-strokeWidth',
|
||||
props.rootEl?.value ?? documentRef.value,
|
||||
{
|
||||
initialValue: `${STYLE_DEFAULTS.strokeWidth}`
|
||||
}
|
||||
)
|
||||
|
||||
const sizeCssVar = useCssVar(
|
||||
'--customize-size',
|
||||
props.rootEl?.value ?? documentRef.value,
|
||||
{
|
||||
initialValue: `${STYLE_DEFAULTS.size}`
|
||||
}
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar, { direction: 'ltr' })
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' })
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr' })
|
||||
|
||||
function resetStyle () {
|
||||
color.value = STYLE_DEFAULTS.color
|
||||
strokeWidth.value = STYLE_DEFAULTS.strokeWidth
|
||||
size.value = STYLE_DEFAULTS.size
|
||||
}
|
||||
|
||||
watch(absoluteStrokeWidth, (enabled) => {
|
||||
const htmlEl = document.documentElement
|
||||
|
||||
htmlEl.classList.toggle('absolute-stroke-width', enabled)
|
||||
})
|
||||
|
||||
const customizingActive = computed(() => {
|
||||
return color.value !== STYLE_DEFAULTS.color
|
||||
|| strokeWidth.value !== STYLE_DEFAULTS.strokeWidth
|
||||
|| size.value !== STYLE_DEFAULTS.size
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="customizer-card" :class="{ customized: customizingActive }">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">
|
||||
Customizer
|
||||
</h2>
|
||||
<ResetButton @click="resetStyle"></ResetButton>
|
||||
</div>
|
||||
<InputField
|
||||
id="icon-color"
|
||||
label="Color"
|
||||
>
|
||||
<template #display>
|
||||
<ColorPicker v-model="color" id="icon-color" class="color-picker"/>
|
||||
</template>
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="stroke-width"
|
||||
label="Stroke width"
|
||||
>
|
||||
<template #display>
|
||||
<span class="customize-label">{{ strokeWidth }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="stroke-width"
|
||||
name="stroke-width"
|
||||
v-model="strokeWidth"
|
||||
:min="0.5"
|
||||
:max="3"
|
||||
:step="0.25"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="size"
|
||||
label="Size"
|
||||
>
|
||||
<template #display>
|
||||
<span class="customize-label">{{ size }}px</span>
|
||||
</template>
|
||||
<RangeSlider
|
||||
id="size"
|
||||
name="size"
|
||||
v-model="size"
|
||||
:min="16"
|
||||
:max="48"
|
||||
:step="4"
|
||||
/>
|
||||
</InputField>
|
||||
|
||||
<InputField
|
||||
id="absolute-stroke-width"
|
||||
label="Absolute Stroke width"
|
||||
>
|
||||
<Switch
|
||||
id="size"
|
||||
name="size"
|
||||
v-model="absoluteStrokeWidth"
|
||||
/>
|
||||
</InputField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
/* margin-bottom: 12px; */
|
||||
}
|
||||
.customizer-card {
|
||||
background: var(--vp-c-bg);
|
||||
padding: 12px 24px 24px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border: 1px solid transparent;
|
||||
transition: border-color .4s ease-in-out;
|
||||
}
|
||||
|
||||
.customizer-card.customized {
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
#absolute-stroke-width {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
</style>
|
||||
28
docs/.vitepress/theme/components/icons/StickyBar.vue
Normal file
28
docs/.vitepress/theme/components/icons/StickyBar.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="search-bar">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
margin-bottom: 16px;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
top: 48px;
|
||||
padding-top: 24px;
|
||||
margin-top: -32px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 32px;
|
||||
background: var(--vp-c-bg);
|
||||
box-shadow: 0 16px 24px var(--vp-c-bg);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.search-bar {
|
||||
padding-top: 32px;
|
||||
top: 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
docs/.vitepress/theme/components/icons/confetti.css
Normal file
104
docs/.vitepress/theme/components/icons/confetti.css
Normal file
@@ -0,0 +1,104 @@
|
||||
.confetti-button {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
--confetti-color: var(--vp-c-brand);
|
||||
--text-color: 0 0 0;
|
||||
}
|
||||
|
||||
.dark .confetti-button {
|
||||
--confetti-color: var(--vp-c-brand-dark);
|
||||
--text-color: 255 255 255;
|
||||
}
|
||||
.confetti-button:before,
|
||||
.confetti-button:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 140%;
|
||||
max-width: 160px;
|
||||
height: 100%;
|
||||
left: -20%;
|
||||
z-index: -1000;
|
||||
transition: all ease-in-out 0.5s;
|
||||
background-repeat: no-repeat;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.confetti-button:before {
|
||||
content: attr(data-confetti-text);
|
||||
letter-spacing: 1px;
|
||||
font-weight: bold;
|
||||
transform: rotate(-8deg);
|
||||
color: rgb(var(--text-color) / 1);
|
||||
display: none;
|
||||
top: -85%;
|
||||
background-image: radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, transparent 20%, var(--confetti-color) 20%, transparent 30%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, transparent 10%, var(--confetti-color) 15%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%);
|
||||
background-size: 10% 10%, 20% 20%, 15% 15%, 20% 20%, 18% 18%, 10% 10%, 15% 15%,
|
||||
10% 10%, 18% 18%;
|
||||
}
|
||||
|
||||
.confetti-button:after {
|
||||
display: none;
|
||||
bottom: -75%;
|
||||
background-image: radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, transparent 10%, var(--confetti-color) 15%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%);
|
||||
background-size: 15% 15%, 20% 20%, 18% 18%, 20% 20%, 15% 15%, 10% 10%, 20% 20%;
|
||||
}
|
||||
|
||||
.confetti-button.animate:before {
|
||||
display: block;
|
||||
animation: topBubbles ease-in-out 1s forwards;
|
||||
}
|
||||
.confetti-button.animate:after {
|
||||
display: block;
|
||||
animation: bottomBubbles ease-in-out 1s forwards;
|
||||
}
|
||||
|
||||
@keyframes topBubbles {
|
||||
0% {
|
||||
color: rgb(var(--text-color) / 0);
|
||||
background-position: 5% 90%, 10% 90%, 10% 90%, 15% 90%, 25% 90%, 25% 90%,
|
||||
40% 90%, 55% 90%, 70% 90%;
|
||||
}
|
||||
30% {
|
||||
color: rgb(var(--text-color) / 1);
|
||||
}
|
||||
50% {
|
||||
background-position: 0% 80%, 0% 20%, 10% 40%, 20% 0%, 30% 30%, 22% 50%,
|
||||
50% 50%, 65% 20%, 90% 30%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 70%, 0% 10%, 10% 30%, 20% -10%, 30% 20%, 22% 40%,
|
||||
50% 40%, 65% 10%, 90% 20%;
|
||||
background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
|
||||
color: rgb(var(--text-color) / 0);
|
||||
}
|
||||
}
|
||||
@keyframes bottomBubbles {
|
||||
0% {
|
||||
background-position: 10% -10%, 30% 10%, 55% -10%, 70% -10%, 85% -10%,
|
||||
70% -10%, 70% 0%;
|
||||
}
|
||||
50% {
|
||||
background-position: 0% 80%, 20% 80%, 45% 60%, 60% 100%, 75% 70%, 95% 60%,
|
||||
105% 0%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 90%, 20% 90%, 45% 70%, 60% 110%, 75% 80%, 95% 70%,
|
||||
110% 10%;
|
||||
background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
|
||||
}
|
||||
}
|
||||
108
docs/.vitepress/theme/components/overrides/VPFooter.vue
Normal file
108
docs/.vitepress/theme/components/overrides/VPFooter.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { theme } = useData()
|
||||
const { hasSidebar } = useSidebar()
|
||||
|
||||
const githubLink = computed(() => theme.value.socialLinks.find(({icon}) => icon === 'github').link)
|
||||
|
||||
const links = computed(() => [
|
||||
{
|
||||
text: 'License',
|
||||
href: '/license'
|
||||
},
|
||||
{
|
||||
text: 'Contribute',
|
||||
href: '/contributing'
|
||||
},
|
||||
{
|
||||
text: 'Changelog',
|
||||
href: `${githubLink.value}/releases`
|
||||
},
|
||||
{
|
||||
text: 'Github',
|
||||
href: `${githubLink.value}/issues`
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
href: `${githubLink.value}/issues`
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer v-if="theme.footer" class="VPFooter" :class="{ 'has-sidebar': hasSidebar }">
|
||||
<div class="container">
|
||||
<p v-if="theme.footer.message" class="message" v-html="theme.footer.message"></p>
|
||||
<p v-if="theme.footer.copyright" class="copyright" v-html="theme.footer.copyright"></p>
|
||||
<div class="links">
|
||||
<VPLink v-for="link in links" :href="link.href" :key="link.text" :rel="link.href.startsWith('http') ? 'noreferrer noopener': undefined">
|
||||
{{ link.text }}
|
||||
</VPLink>
|
||||
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss" rel="noreferrer noopener">
|
||||
<img src="/vercel.svg" alt="Powered by Vercel" width="200" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPFooter {
|
||||
position: relative;
|
||||
z-index: var(--vp-z-index-footer);
|
||||
border-top: 1px solid var(--vp-c-gutter);
|
||||
padding: 32px 24px;
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.VPFooter.has-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: var(--vp-layout-max-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.message,
|
||||
.copyright {
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.message { order: 2; }
|
||||
.copyright { order: 1; }
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1152px) {
|
||||
.VPFooter {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.links {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { menu } from '../../../data/iconNodes'
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon'
|
||||
|
||||
const Menu = createLucideIcon('menu', menu)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menu />
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
import packageData from '../../../data/packageData.json';
|
||||
import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
|
||||
import fetchPackages from "../../../lib/fetchPackages";
|
||||
|
||||
export default {
|
||||
async load() {
|
||||
const packages = await fetchPackages();
|
||||
return {
|
||||
packages: packages
|
||||
.filter(p => p.name in packageData)
|
||||
.map((pData) => ({
|
||||
...pData,
|
||||
...packageData[pData.name],
|
||||
documentation: `/guide/packages/${pData.name}`,
|
||||
source: `https://github.com/lucide-icons/lucide/tree/main/packages/${pData.name}`,
|
||||
icon: `/framework-logos/${packageData[pData.name].icon}.svg`,
|
||||
})).sort((a, b) => a.order - b.order),
|
||||
thirdPartyPackages,
|
||||
};
|
||||
}
|
||||
}
|
||||
64
docs/.vitepress/theme/components/packages/PackageList.vue
Normal file
64
docs/.vitepress/theme/components/packages/PackageList.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import {data} from './PackageList.data'
|
||||
import PackageListItem from "./PackageListItem.vue";</script>
|
||||
|
||||
<template>
|
||||
<section class="package-group">
|
||||
<h1 class="name">Packages</h1>
|
||||
<div class="grid package-list" ref="container">
|
||||
<div v-for="packageData in data.packages" class="item">
|
||||
<PackageListItem :packageData="packageData"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="package-group">
|
||||
<h2 class="name">Third-party packages</h2>
|
||||
<div class="grid package-list" ref="container">
|
||||
<div v-for="packageData in data.thirdPartyPackages" class="item">
|
||||
<PackageListItem :packageData="packageData"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.name {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.package-group {
|
||||
margin-bottom: 96px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-content: space-evenly;
|
||||
box-sizing: border-box;
|
||||
margin: -8px;
|
||||
}
|
||||
|
||||
.grid > * {
|
||||
flex-basis: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.grid > * {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.grid > * {
|
||||
flex-basis: 33.33%;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
110
docs/.vitepress/theme/components/packages/PackageListItem.vue
Normal file
110
docs/.vitepress/theme/components/packages/PackageListItem.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vitepress';
|
||||
import {PackageItem} from "../../types";
|
||||
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
|
||||
|
||||
const { go } = useRouter()
|
||||
const props = defineProps<{
|
||||
packageData: PackageItem,
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="package">
|
||||
<header class="package-header">
|
||||
<div class="package-icon-well">
|
||||
<img :src="packageData.icon" alt="" class="package-icon" :class="{[packageData.iconClass]: true, light: packageData.iconDark}" />
|
||||
<img v-if="packageData.iconDark" :src="packageData.iconDark" alt="" class="package-icon dark" :class="packageData.iconClass" />
|
||||
</div>
|
||||
<div class="package-title">
|
||||
<h2 class="title">{{ props.packageData.name }}</h2>
|
||||
<a v-for="shield in props.packageData.shields" :href="shield.href" class="package-shield" rel="noreferrer noopener">
|
||||
<img :src="shield.src" :alt="shield.href" />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="package-details">
|
||||
{{ packageData.description }}
|
||||
</div>
|
||||
<footer class="package-footer">
|
||||
<VPButton
|
||||
:href="packageData.documentation"
|
||||
text="Guide"
|
||||
theme="brand"
|
||||
@click="go(packageData.documentation)"
|
||||
/>
|
||||
<VPButton
|
||||
:href="packageData.source"
|
||||
text="Source"
|
||||
theme="alt"
|
||||
@click="go(packageData.source)"
|
||||
/>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.package {
|
||||
border: 1px solid var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
}
|
||||
.package {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
.package-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
}
|
||||
@media screen and (min-width: 480px) {
|
||||
.package-header {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.package-icon-well {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
.package-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
h2.title {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.package-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.package-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.package-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
html.dark .package-icon-invert {
|
||||
filter: invert(1) hue-rotate(180deg);
|
||||
}
|
||||
html.dark .package-icon.light {
|
||||
display: none;
|
||||
}
|
||||
html:not(.dark) .package-icon.dark {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
87
docs/.vitepress/theme/composables/useActiveAnchor.ts
Normal file
87
docs/.vitepress/theme/composables/useActiveAnchor.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { onMounted, onUpdated, onUnmounted } from 'vue';
|
||||
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils'
|
||||
|
||||
/*
|
||||
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
|
||||
*/
|
||||
|
||||
export function useActiveAnchor(container, marker) {
|
||||
const onScroll = throttleAndDebounce(setActiveLink, 100);
|
||||
let prevActiveLink = null;
|
||||
onMounted(() => {
|
||||
requestAnimationFrame(setActiveLink);
|
||||
window.addEventListener('scroll', onScroll);
|
||||
});
|
||||
onUpdated(() => {
|
||||
// sidebar update means a route change
|
||||
activateLink(location.hash);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
});
|
||||
function setActiveLink() {
|
||||
const links = [].slice.call(container.value.querySelectorAll('.outline-link'));
|
||||
const anchors = [].slice
|
||||
.call(document.querySelectorAll('.content .header-anchor'))
|
||||
.filter((anchor) => {
|
||||
return links.some((link) => {
|
||||
return link.hash === anchor.hash && anchor.offsetParent !== null;
|
||||
});
|
||||
});
|
||||
const scrollY = window.scrollY;
|
||||
const innerHeight = window.innerHeight;
|
||||
const offsetHeight = document.body.offsetHeight;
|
||||
const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;
|
||||
// page bottom - highlight last one
|
||||
if (anchors.length && isBottom) {
|
||||
activateLink(anchors[anchors.length - 1].hash);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < anchors.length; i++) {
|
||||
const anchor = anchors[i];
|
||||
const nextAnchor = anchors[i + 1];
|
||||
const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor);
|
||||
if (isActive) {
|
||||
activateLink(hash);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
function activateLink(hash) {
|
||||
if (prevActiveLink) {
|
||||
prevActiveLink.classList.remove('active');
|
||||
}
|
||||
if (hash !== null) {
|
||||
prevActiveLink = container.value.querySelector(`a[href="${decodeURIComponent(hash)}"]`);
|
||||
}
|
||||
const activeLink = prevActiveLink;
|
||||
if (activeLink) {
|
||||
activeLink.classList.add('active');
|
||||
marker.value.style.top = activeLink.offsetTop + 5 + 'px';
|
||||
marker.value.style.opacity = '1';
|
||||
}
|
||||
else {
|
||||
marker.value.style.top = '33px';
|
||||
marker.value.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PAGE_OFFSET = 64;
|
||||
|
||||
function getAnchorTop(anchor) {
|
||||
return anchor.parentElement.offsetTop - PAGE_OFFSET;
|
||||
}
|
||||
function isAnchorActive(index, anchor, nextAnchor) {
|
||||
const scrollTop = window.scrollY;
|
||||
if (index === 0 && scrollTop === 0) {
|
||||
return [true, null];
|
||||
}
|
||||
if (scrollTop < getAnchorTop(anchor)) {
|
||||
return [false, null];
|
||||
}
|
||||
if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) {
|
||||
return [true, anchor.hash];
|
||||
}
|
||||
return [false, null];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user