mirror of
https://github.com/lucide-icons/lucide.git
synced 2025-12-17 00:37:42 +01:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
bf530d39d3 | ||
|
|
71e8df6354 | ||
|
|
cafd2a838b | ||
|
|
e16f368502 | ||
|
|
d38509a03d | ||
|
|
6550e22874 | ||
|
|
22193420c7 | ||
|
|
70827d4571 | ||
|
|
7d980f6cc1 | ||
|
|
67131489c8 | ||
|
|
ebf03a5434 | ||
|
|
8fda42c719 | ||
|
|
b17627b82d | ||
|
|
84ec1620a8 | ||
|
|
a87ae2a92b |
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
|
||||
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
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -20,3 +20,16 @@ packages/**/src/aliases.ts
|
||||
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/relatedIcons.json
|
||||
docs/.vercel
|
||||
docs/.nitro
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
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
|
||||
})
|
||||
29
docs/.vitepress/api/icon-nodes/index.get.ts
Normal file
29
docs/.vitepress/api/icon-nodes/index.get.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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')
|
||||
|
||||
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
|
||||
}, {})
|
||||
})
|
||||
44
docs/.vitepress/api/icons/[iconName].get.ts
Normal file
44
docs/.vitepress/api/icons/[iconName].get.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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')
|
||||
|
||||
return svg
|
||||
|
||||
})
|
||||
10
docs/.vitepress/api/tags/index.get.ts
Normal file
10
docs/.vitepress/api/tags/index.get.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { eventHandler, setResponseHeader } from 'h3'
|
||||
import iconMetaData from '../../data/iconMetaData'
|
||||
|
||||
export default eventHandler((event) => {
|
||||
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
|
||||
|
||||
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' }
|
||||
})
|
||||
160
docs/.vitepress/config.ts
Normal file
160
docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
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';
|
||||
|
||||
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://plausible.io/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 === 'index.md') {
|
||||
console.log('Home!');
|
||||
}
|
||||
|
||||
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]
|
||||
})
|
||||
55
docs/.vitepress/lib/icons.ts
Normal file
55
docs/.vitepress/lib/icons.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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 function getAllNames() {
|
||||
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
|
||||
|
||||
return fileNames
|
||||
.filter((fileName) => fs.existsSync(directory + '/' + path.basename(fileName, '.json') + '.svg'))
|
||||
.map((fileName) => path.basename(fileName, '.json'));
|
||||
}
|
||||
|
||||
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 = getAllNames();
|
||||
|
||||
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>
|
||||
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 you 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" />
|
||||
123
docs/.vitepress/theme/components/icons/CopySVGButton.vue
Normal file
123
docs/.vitepress/theme/components/icons/CopySVGButton.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import ButtonMenu from '../base/ButtonMenu.vue'
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import useConfetti from '../../composables/useConfetti';
|
||||
|
||||
const allowedAttrs = [
|
||||
'xmlns',
|
||||
'width',
|
||||
'height',
|
||||
'viewBox',
|
||||
'fill',
|
||||
'stroke',
|
||||
'stroke-width',
|
||||
'stroke-linecap',
|
||||
'stroke-linejoin',
|
||||
'class',
|
||||
]
|
||||
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 getSVGIcon() {
|
||||
const svg = document.querySelector('#previewer svg')
|
||||
if (!svg) return
|
||||
|
||||
const clonedSvg = svg.cloneNode(true) as SVGElement
|
||||
|
||||
// Filter out attributes that are not allowed in SVGs
|
||||
for (const attr of Array.from(clonedSvg.attributes)) {
|
||||
if (!allowedAttrs.includes(attr.name)) {
|
||||
clonedSvg.removeAttribute(attr.name)
|
||||
}
|
||||
}
|
||||
|
||||
const svgString = new XMLSerializer().serializeToString(clonedSvg)
|
||||
|
||||
return svgString
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.download = `${props.name}.svg`;
|
||||
link.href = `data:image/svg+xml;base64,${btoa(svgString)}`
|
||||
link.click();
|
||||
|
||||
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);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.download = `${props.name}.png`;
|
||||
link.href = canvas.toDataURL('image/png')
|
||||
link.click();
|
||||
|
||||
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>
|
||||
154
docs/.vitepress/theme/components/icons/IconDetailOverlay.vue
Normal file
154
docs/.vitepress/theme/components/icons/IconDetailOverlay.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<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';
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const isOpen = computed(() => !!props.icon)
|
||||
|
||||
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: 0 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>
|
||||
47
docs/.vitepress/theme/components/icons/IconGrid.vue
Normal file
47
docs/.vitepress/theme/components/icons/IconGrid.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<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;
|
||||
}
|
||||
</style>
|
||||
83
docs/.vitepress/theme/components/icons/IconInfo.vue
Normal file
83
docs/.vitepress/theme/components/icons/IconInfo.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<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) return []
|
||||
return props.icon.tags.join(' • ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-info">
|
||||
<IconDetailName class="icon-name">
|
||||
{{ icon.name }}
|
||||
</IconDetailName>
|
||||
<p class="icon-tags">
|
||||
{{ tags }}
|
||||
</p>
|
||||
<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;
|
||||
margin-top: 0;;
|
||||
margin-bottom: 16px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
154
docs/.vitepress/theme/components/icons/IconItem.vue
Normal file
154
docs/.vitepress/theme/components/icons/IconItem.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import { useRouter } from 'vitepress';
|
||||
|
||||
export type IconNode = [elementName: string, attrs: Record<string, string>][]
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
// tags: string[];
|
||||
// categories: 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 icon = createLucideIcon(props.name, props.iconNode)
|
||||
|
||||
function navigateToIcon() {
|
||||
if(props.overlayMode && showOverlay.value) {
|
||||
window.history.pushState({}, '', `/icons/${props.name}`)
|
||||
emit('setActiveIcon', props.name)
|
||||
}
|
||||
else {
|
||||
go(`/icons/${props.name}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="icon-button"
|
||||
@click="navigateToIcon"
|
||||
:class="{ 'active' : active }"
|
||||
:data-title="name"
|
||||
:aria-label="name"
|
||||
:href="`/icons/${props.name}`"
|
||||
>
|
||||
<KeepAlive>
|
||||
<component
|
||||
v-if="!hideIcon"
|
||||
:is="icon"
|
||||
class="lucide-icon"
|
||||
:class="{ customizable }"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<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);
|
||||
}
|
||||
|
||||
.icon-button:hover:before {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 48px) scale(1);
|
||||
}
|
||||
|
||||
.icon-button:before {
|
||||
content: attr(data-title);
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
margin-left: 27px;
|
||||
transform: translate(-50%, 48px) scale(0.9);
|
||||
font-weight: 400;
|
||||
position: absolute;
|
||||
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;
|
||||
/* max-width: calc((32px * 2) + 56px); */
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.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>
|
||||
103
docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
Normal file
103
docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<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'
|
||||
|
||||
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 searchResults = useSearch(searchQuery, props.icons, [
|
||||
{ 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)
|
||||
})
|
||||
|
||||
const activeIcon = computed(() =>
|
||||
props.icons?.find((icon) => icon.name === activeIconName.value)
|
||||
)
|
||||
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
:icon="activeIcon"
|
||||
@close="setActiveIconName('')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar.category-search {
|
||||
margin-bottom: -54px;
|
||||
}
|
||||
</style>
|
||||
124
docs/.vitepress/theme/components/icons/IconsOverview.vue
Normal file
124
docs/.vitepress/theme/components/icons/IconsOverview.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<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'
|
||||
|
||||
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 * 16;
|
||||
}
|
||||
if(isLargeScreen.value) {
|
||||
return 16 * 12;
|
||||
}
|
||||
if(isMediumScreen.value) {
|
||||
return 13 * 12;
|
||||
}
|
||||
|
||||
if(isSmallScreen.value) {
|
||||
return 10 * 10;
|
||||
}
|
||||
|
||||
return 10 * 5;
|
||||
})
|
||||
|
||||
const { searchInput, searchQuery, searchQueryThrottled } = useSearchInput()
|
||||
const searchResults = useSearch(searchQueryThrottled, props.icons, [
|
||||
{ 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
|
||||
}
|
||||
|
||||
const activeIcon = computed(() => props.icons.find((icon) => icon.name === activeIconName.value))
|
||||
|
||||
watch(searchQueryThrottled, (searchString) => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
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"
|
||||
/>
|
||||
</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"
|
||||
:icon="activeIcon"
|
||||
@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>
|
||||
77
docs/.vitepress/theme/components/icons/NoResults.vue
Normal file
77
docs/.vitepress/theme/components/icons/NoResults.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<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')"/>
|
||||
or
|
||||
<VPButton text="Check if someone has already requested this icon"
|
||||
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;
|
||||
}
|
||||
</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>
|
||||
155
docs/.vitepress/theme/components/icons/SidebarIconCustomizer.vue
Normal file
155
docs/.vitepress/theme/components/icons/SidebarIconCustomizer.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, type Ref, watch } from 'vue'
|
||||
import { useCssVar, syncRef } from '@vueuse/core'
|
||||
import { 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: 'default'
|
||||
}
|
||||
)
|
||||
|
||||
const strokeWidthCssVar = useCssVar(
|
||||
'--customize-strokeWidth',
|
||||
props.rootEl?.value ?? documentRef.value,
|
||||
{
|
||||
initialValue: '2'
|
||||
}
|
||||
)
|
||||
|
||||
const sizeCssVar = useCssVar(
|
||||
'--customize-size',
|
||||
props.rootEl?.value ?? documentRef.value,
|
||||
{
|
||||
initialValue: '24'
|
||||
}
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar, { direction: 'ltr' })
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' })
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr' })
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
strokeWidth.value = 2
|
||||
size.value = 24
|
||||
}
|
||||
|
||||
watch(absoluteStrokeWidth, (enabled) => {
|
||||
const htmlEl = document.documentElement
|
||||
|
||||
htmlEl.classList.toggle('absolute-stroke-width', enabled)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="customizer-card">
|
||||
<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;
|
||||
}
|
||||
|
||||
.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];
|
||||
}
|
||||
25
docs/.vitepress/theme/composables/useCategoryView.ts
Normal file
25
docs/.vitepress/theme/composables/useCategoryView.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ref, inject, Ref
|
||||
} from 'vue';
|
||||
|
||||
export const CATEGORY_VIEW_CONTEXT = Symbol('categoryView');
|
||||
|
||||
interface CategoryViewContext {
|
||||
selectedCategory: Ref<string>
|
||||
categoryCounts: Ref<Record<string, number>>
|
||||
}
|
||||
|
||||
export const categoryViewContext = {
|
||||
selectedCategory: ref(''),
|
||||
categoryCounts: ref({}),
|
||||
};
|
||||
|
||||
export function useCategoryView(): CategoryViewContext {
|
||||
const context = inject<CategoryViewContext>(CATEGORY_VIEW_CONTEXT);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCategoryView must be used with categoryView context');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
18
docs/.vitepress/theme/composables/useConfetti.ts
Normal file
18
docs/.vitepress/theme/composables/useConfetti.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export default function useConfetti() {
|
||||
const animate = ref(false)
|
||||
|
||||
function confetti() {
|
||||
animate.value = true;
|
||||
|
||||
setTimeout(function () {
|
||||
animate.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
animate,
|
||||
confetti
|
||||
}
|
||||
}
|
||||
30
docs/.vitepress/theme/composables/useIconStyle.ts
Normal file
30
docs/.vitepress/theme/composables/useIconStyle.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import {
|
||||
ref, inject, Ref
|
||||
} from 'vue';
|
||||
|
||||
export const ICON_STYLE_CONTEXT = Symbol('size');
|
||||
|
||||
interface IconSizeContext {
|
||||
size: Ref<number>
|
||||
strokeWidth: Ref<number>
|
||||
color: Ref<string>
|
||||
}
|
||||
|
||||
export const iconStyleContext = {
|
||||
size: ref(24),
|
||||
strokeWidth: ref(2),
|
||||
color: ref('currentColor'),
|
||||
absoluteStrokeWidth: ref(false),
|
||||
};
|
||||
|
||||
export function useIconStyleContext(): IconSizeContext{
|
||||
const context = inject<IconSizeContext>(ICON_STYLE_CONTEXT);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useIconStyleContext must be used with useIconStyleProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
23
docs/.vitepress/theme/composables/useSearch.ts
Normal file
23
docs/.vitepress/theme/composables/useSearch.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import { shallowRef, computed, Ref } from 'vue';
|
||||
|
||||
const useSearch = <T>(query: Ref<string>, collection: T[], keys: Fuse.FuseOptionKey<T>[] = []) => {
|
||||
const index = shallowRef(
|
||||
new Fuse(collection, {
|
||||
threshold: 0.2,
|
||||
keys,
|
||||
})
|
||||
)
|
||||
|
||||
const results = computed(() => {
|
||||
if (query.value) {
|
||||
return index.value.search(query.value).map((result) => result.item);
|
||||
}
|
||||
|
||||
return collection;
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export default useSearch;
|
||||
40
docs/.vitepress/theme/composables/useSearchInput.ts
Normal file
40
docs/.vitepress/theme/composables/useSearchInput.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { refThrottled } from '@vueuse/core';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const useSearchInput = () => {
|
||||
const searchInput = ref()
|
||||
const searchQuery = ref(
|
||||
typeof window === 'undefined'
|
||||
? ''
|
||||
: (
|
||||
new URLSearchParams(window.location.search).get('search')
|
||||
|| ''
|
||||
)
|
||||
)
|
||||
const searchQueryThrottled = refThrottled(searchQuery, 200)
|
||||
|
||||
watch(searchQueryThrottled, (searchString) => {
|
||||
const newUrl = new URL(window.location.href);
|
||||
|
||||
newUrl.searchParams.set('search', searchString);
|
||||
|
||||
nextTick(() => {
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if(searchParams.has('focus')) {
|
||||
searchInput.value.focus()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
searchInput,
|
||||
searchQuery,
|
||||
searchQueryThrottled
|
||||
};
|
||||
};
|
||||
|
||||
export default useSearchInput;
|
||||
26
docs/.vitepress/theme/index.ts
Normal file
26
docs/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { h } from 'vue'
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './style.css'
|
||||
import { Theme } from 'vitepress'
|
||||
import IconsSidebarNavAfter from './layouts/IconsSidebarNavAfter.vue'
|
||||
import HomeHeroIconsCard from './components/home/HomeHeroIconsCard.vue'
|
||||
import HomeHeroBefore from "./components/home/HomeHeroBefore.vue";
|
||||
import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle'
|
||||
import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView'
|
||||
|
||||
const theme: Partial<Theme> = {
|
||||
extends: DefaultTheme,
|
||||
Layout() {
|
||||
return h(DefaultTheme.Layout, null, {
|
||||
'home-hero-before': () => h(HomeHeroBefore),
|
||||
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
|
||||
'home-hero-image': () => h(HomeHeroIconsCard),
|
||||
})
|
||||
},
|
||||
enhanceApp({ app }) {
|
||||
app.provide(ICON_STYLE_CONTEXT, iconStyleContext)
|
||||
app.provide(CATEGORY_VIEW_CONTEXT, categoryViewContext)
|
||||
}
|
||||
}
|
||||
|
||||
export default theme
|
||||
16
docs/.vitepress/theme/layouts/IconsSidebarNavAfter.vue
Normal file
16
docs/.vitepress/theme/layouts/IconsSidebarNavAfter.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
import CategoryList from '../components/icons/CategoryList.vue'
|
||||
import SidebarIconCustomizer from '../components/icons/SidebarIconCustomizer.vue'
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SidebarIconCustomizer v-if="page?.relativePath?.startsWith?.('icons')"/>
|
||||
<CategoryList v-if="page?.relativePath?.startsWith?.('icons')"/>
|
||||
</div>
|
||||
</template>
|
||||
128
docs/.vitepress/theme/style.css
Normal file
128
docs/.vitepress/theme/style.css
Normal file
@@ -0,0 +1,128 @@
|
||||
:root {
|
||||
--vp-c-brand: #F56565;
|
||||
--vp-c-brand-light: #F67373;
|
||||
--vp-c-brand-lighter: #F89191;
|
||||
--vp-c-brand-dark: #DC5A5A;
|
||||
--vp-c-brand-darker: #C45050;
|
||||
|
||||
--vp-c-bg-alt-up: #fff;
|
||||
--vp-c-bg-alt-down: #fff;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg-alt-up: #1B1B1D;
|
||||
--vp-c-bg-alt-down: #0F0F10;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .logo {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.VPHomeHero > .container {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.VPHomeHero .image-container {
|
||||
transform: none;
|
||||
width: 100%;
|
||||
/* padding: 0 24px; */
|
||||
}
|
||||
|
||||
/* .VPHomeHero .container {
|
||||
flex-direction: column-reverse;
|
||||
} */
|
||||
.VPHomeHero .container .main {
|
||||
/* flex:1; */
|
||||
flex-shirk: 0;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .main h1.name {
|
||||
color: var(--vp-c-text);
|
||||
|
||||
}
|
||||
.VPHomeHero .container .main h1.name .clip {
|
||||
color: inherit;
|
||||
-webkit-text-fill-color: unset;
|
||||
color: var(--vp-c-text);
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .main h1::first-line {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
|
||||
/* */
|
||||
.VPHomeHero .container .image {
|
||||
margin: 0;
|
||||
order: 2;
|
||||
/* flex: 1; */
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .image-container {
|
||||
height: auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .image-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.VPFeature .icon {
|
||||
background-color: var(--vp-c-bg);;
|
||||
}
|
||||
|
||||
.vp-doc[class*=" _icons_"] > div {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.VPDoc:has(.vp-doc[class*=" _icons_"]) > .container > .content{
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.VPHomeHero .container .main h1.name .clip {
|
||||
font-size: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
|
||||
.VPHomeHero .container .image {
|
||||
order: 1;
|
||||
margin-bottom: auto;
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .main {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .image {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .image-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.VPHomeHero .container .main h1.name {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.VPNavBarHamburger .container > span {
|
||||
border-radius: 2px;
|
||||
}
|
||||
/*
|
||||
html:has(* .outline-link:target) {
|
||||
scroll-behavior: smooth;
|
||||
} */
|
||||
46
docs/.vitepress/theme/types.ts
Normal file
46
docs/.vitepress/theme/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export type IconNode = [elementName: string, attrs: Record<string, string>][]
|
||||
export type IconNodeWithKeys = [elementName: string, attrs: Record<string, string>, key: string][]
|
||||
|
||||
export interface IconEntity {
|
||||
name: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
contributors: string[];
|
||||
aliases?: string[];
|
||||
iconNode: IconNode;
|
||||
createdRelease?: Release;
|
||||
changedRelease?: Release;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
name: string
|
||||
title: string
|
||||
icon?: string
|
||||
iconCount: number
|
||||
icons?: IconEntity[]
|
||||
}
|
||||
|
||||
interface Shield {
|
||||
alt: string
|
||||
src: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export interface PackageItem {
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
iconDark: string
|
||||
shields: Shield[]
|
||||
source: string
|
||||
documentation: string
|
||||
order?: number
|
||||
private?: boolean
|
||||
flutter?: object
|
||||
}
|
||||
|
||||
|
||||
export interface Release {
|
||||
version: string
|
||||
date: string
|
||||
}
|
||||
10
docs/.vitepress/vue-shim.d.ts
vendored
Normal file
10
docs/.vitepress/vue-shim.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module "*.vue" {
|
||||
import Vue from "vue";
|
||||
export default Vue;
|
||||
}
|
||||
|
||||
declare module "*.data.ts" {
|
||||
const data: any;
|
||||
|
||||
export { data };
|
||||
}
|
||||
37
docs/README.txt
Normal file
37
docs/README.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
# Lucide Docs website
|
||||
|
||||
The Lucide docs website is built with Vitepress: https://vitepress.dev/
|
||||
This is Markdown-based documentation powered by Vue.
|
||||
|
||||
This is why this file is in txt format.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
```sh
|
||||
# Start docs dev server
|
||||
pnpm docs:dev
|
||||
|
||||
# Start api dev server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
# Build docs
|
||||
pnpm docs:build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build api
|
||||
pnpm build:api
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
See .vitepress directory.
|
||||
5
docs/code-of-conduct.md
Normal file
5
docs/code-of-conduct.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
<!--@include: ../CODE_OF_CONDUCT.md -->
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
title: Comparison
|
||||
---
|
||||
|
||||
# Comparison
|
||||
|
||||
## Lucide vs Feather Icons
|
||||
|
||||
Lucide is a community-run fork of [Feather Icons](https://github.com/feathericons/feather).
|
||||
|
||||
It began after growing disaffection of the [Feather Icons](https://github.com/feathericons/feather) project moderation. With over 300+ open issues and over 100+ open PRs, the Feather Icons project has been abandoned and not maintained actively. This unfortunately means that hundreds of developers and designers wasted their time contributing to Feather Icons with no chance of PRs being accepted.
|
||||
|
||||
Lucide is trying to expand the icon set as much as possible while staying faithful to the original simplistic design language. We do this as a community of devs and designers.
|
||||
|
||||
### Why should I choose Lucide over Feather Icons?
|
||||
|
||||
- Lucide already expended the icon set by 130+ in less then a year. Lucide has over 500+ icon, feather sticks around 286 icons.
|
||||
- Well maintained code base.
|
||||
- Active community.
|
||||
|
||||
### Should I migrate to Lucide?
|
||||
|
||||
That depends if you're fine with the icons from feather icons. If that is the case, it is maybe not the effort worth it.
|
||||
But if you keep wrestling and feel limited by the icons Feather provides you can consider to migrate.
|
||||
We didn't remove any icons when we forked, but there are some icons renamed.
|
||||
5
docs/contributing.md
Normal file
5
docs/contributing.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
aside: false
|
||||
---
|
||||
|
||||
<!--@include: ../CONTRIBUTING.md -->
|
||||
25
docs/guide/comparison.md
Normal file
25
docs/guide/comparison.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Comparison
|
||||
---
|
||||
|
||||
# Comparison
|
||||
|
||||
## Lucide vs Feather Icons
|
||||
|
||||
Lucide is a community-driven fork of [Feather Icons](https://github.com/feathericons/feather).
|
||||
|
||||
The decision to create Lucide arose from growing dissatisfaction with the moderation of the Feather Icons project. With more than 300 open issues and over 100 open PRs, the Feather Icons project has been abandoned and is no longer actively maintained. Unfortunately, this means that numerous developers and designers have invested their time in contributing to Feather Icons without the possibility of their PRs being accepted.
|
||||
|
||||
In an effort to expand the icon set while remaining true to the original minimalist design language, Lucide is driven by a community of developers and designers. We strive to grow together and maintain a faithful continuation of the project.
|
||||
|
||||
### Why should I choose Lucide over Feather Icons?
|
||||
|
||||
- Lucide has expanded its icon set by 500+ in the last few years. Lucide now has over 1000 icons, while Feather has around 287 icons.
|
||||
- Well maintained code base.
|
||||
- Active community.
|
||||
|
||||
### Should I migrate to Lucide?
|
||||
|
||||
That depends on whether you're satisfied with the icons from Feather Icons. If that is the case, it may not be worth the effort.
|
||||
However, if you find yourself struggling and feeling limited by the icons provided by Feather, you can consider migrating.
|
||||
When we forked, we didn't remove any icons, but some icons have been renamed.
|
||||
@@ -28,7 +28,7 @@ Set the following:
|
||||
1. Stroke width: 2px
|
||||
2. Stroke alignment: center
|
||||
|
||||

|
||||

|
||||
|
||||
## Export Or Copy Your Icon
|
||||
Once you have completed your icon, you can export it.
|
||||
@@ -21,35 +21,35 @@ Here are rules that should be followed to keep quality and consistency when maki
|
||||
|
||||
### 1. Icons must be designed on a 24 by 24 pixels canvas.
|
||||
|
||||

|
||||

|
||||
|
||||
### 2. Icons must have at least 1 pixel padding within the canvas.
|
||||
|
||||

|
||||

|
||||
|
||||
### 3. Icons must have a stroke width of 2 pixels.
|
||||
|
||||

|
||||

|
||||
|
||||
### 4. Icons must use round joins.
|
||||
|
||||

|
||||

|
||||
|
||||
### 5. Icons must use round caps.
|
||||
|
||||

|
||||

|
||||
|
||||
### 6. Icons must use centered strokes.
|
||||
|
||||

|
||||

|
||||
|
||||
### 7. Shapes (such as squares) in icons must have border radius of 2 pixels.
|
||||
|
||||

|
||||

|
||||
|
||||
### 8. Distinct elements must have 2 pixels of spacing between each other.
|
||||
|
||||

|
||||

|
||||
|
||||
## Code Conventions
|
||||
|
||||
@@ -24,7 +24,7 @@ The Illustrator template is created following guidelines from the [Icon Design G
|
||||
|
||||
5. Export the file with the export menu under: `Export > Export As..` than safe the file as SVG. Select the following options in the SVG Options dialog:
|
||||
|
||||

|
||||

|
||||
|
||||
After that, double check that the [code conventions and SVG global attributes](icon-design-guide.md#code-conventions) are correct.
|
||||
|
||||
@@ -13,11 +13,11 @@ When opening a new document, Inkscape will create a canvas of a default size. T
|
||||
|
||||
1. Open the Document Properties dialog (File -> Document Properties).
|
||||
2. On the “Page Size” tab, under “Custom Size” set the Units to `px` and set both Height and Width to 24.
|
||||

|
||||

|
||||
3. On the “Grid” tab, select `Rectangular Grid` and click “New Grid”.
|
||||

|
||||

|
||||
4. Set the Grid Units to `px` and set Spacing X and Spacing Y both to 1.
|
||||

|
||||

|
||||
5. Close the Document Properties dialog.
|
||||
6. To center the canvas in the viewport, select View -> Zoom -> Drawing.
|
||||
|
||||
@@ -25,17 +25,17 @@ When opening a new document, Inkscape will create a canvas of a default size. T
|
||||
|
||||
1. Create a path or shape.
|
||||
2. With the path selected, open the Stroke and Fill panel by pressing `Ctrl+Shift+F` on your keyboard.
|
||||

|
||||

|
||||
3. On the “Stroke Style” tab:
|
||||
* Set Stroke Width to `2px`.
|
||||
* Select the rounded join type.
|
||||
* Select the rounded cap type.
|
||||
4. If the shape is a rectangle, select the rectangle and in the top of the screen below the menu bar, set `Rx` and `Ry` to `2px`.
|
||||

|
||||

|
||||
|
||||
## Saving A File
|
||||
|
||||
1. When ready to save the file, click Save As and select “Optimized SVG” as the file type.
|
||||

|
||||

|
||||
2. After clicking Save, to conform with the other icons in the package, set Pretty Printing to use spaces and set the indentation depth to 2.
|
||||

|
||||

|
||||
28
docs/guide/index.md
Normal file
28
docs/guide/index.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: What is Lucide?
|
||||
nextPage:
|
||||
- comparison
|
||||
- installation
|
||||
---
|
||||
|
||||
# What is Lucide?
|
||||
|
||||
Lucide is an open-source icon library that provides 1000+ vector (svg) files for displaying icons and symbols in digital and non-digital projects. The library aims to make it easier for designers and developers to incorporate icons into their projects by providing several official [packages](/packages) to make it easier to use these icons in your project.
|
||||
|
||||
## Available Icons
|
||||
|
||||
Lucide contains icons with different variants and states, allowing users to choose the most suitable icon for their needs. And if a desired icon isn't available yet, users can open a design request, and the Lucide community contributors will help provide new icons. With more icons to choose from, users have more options to work with in their projects.
|
||||
Complete Set of Icons
|
||||
|
||||
As new applications with specific features arise, Lucide aims to provide a complete set of icons for every project. The community follows a set of design rules when designing new icons. These rules maintain standards for the icons, such as recognizability, consistency in style, and readability at all sizes. While creativity is valued in new icons, recognizable design conventions are important to ensure that the icons are easily identifiable by users.
|
||||
|
||||
## Code Optimization
|
||||
|
||||
In addition to design, code is also important. Assets like icons can significantly increase bandwidth usage in web projects. With the growing internet, Lucide has a responsibility to keep their assets as small as possible. To achieve this, Lucide uses SVG compression and specific code architecture for tree-shaking abilities. After tree-shaking, you only ship the icons you used, which helps to keep software distribution size to a minimum.
|
||||
|
||||
## Official Packages
|
||||
|
||||
Lucide's official packages are designed to work on different platforms, making it easier for users to integrate icons into their projects. The packages are available for various technologies, including [Web (Vanilla)](https://lucide.dev/guide/packages/lucide), [React](https://lucide.dev/guide/packages/lucide-react), [React Native](https://lucide.dev/guide/packages/lucide-react-native), [Vue](https://lucide.dev/guide/packages/lucide-vue), [Vue 3](https://lucide.dev/guide/packages/lucide-vue-next), [Svelte](https://lucide.dev/guide/packages/lucide-svelte),[Preact](https://lucide.dev/guide/packages/lucide-preact), [Solid](https://lucide.dev/guide/packages/lucide-solid), [Angular](https://lucide.dev/guide/packages/lucide-angular), [NodeJS](https://lucide.dev/guide/packages/lucide-static#nodejs) and [Flutter](https://lucide.dev/guide/packages/lucide-flutter).
|
||||
|
||||
## Community
|
||||
If you have any questions about Lucide, feel free to reach out to the community. You can find them on [GitHub](https://github.com/lucide-icons/lucide) and [Discord](https://discord.gg/EH6nSts).
|
||||
@@ -8,15 +8,21 @@ title: Installation
|
||||
|
||||
Implementation of the lucide icon library for web applications.
|
||||
|
||||
```bash
|
||||
::: code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm install lucide
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn add lucide
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm install lucide
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
yarn add lucide
|
||||
```
|
||||
:::
|
||||
|
||||
For more details, see the [documentation](packages/lucide.md).
|
||||
|
||||
@@ -24,16 +30,22 @@ For more details, see the [documentation](packages/lucide.md).
|
||||
|
||||
Implementation of the lucide icon library for react applications.
|
||||
|
||||
```bash
|
||||
::: code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm install lucide-react
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn add lucide-react
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
```sh [npm]
|
||||
npm install lucide-react
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
For more details, see the [documentation](packages/lucide-react.md).
|
||||
|
||||
## Vue 2
|
||||
109
docs/guide/packages/lucide-angular.md
Normal file
109
docs/guide/packages/lucide-angular.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Lucide Angular
|
||||
|
||||
Implementation of the lucide icon library for Angular applications.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
yarn add lucide-angular
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
npm install lucide-angular
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
### Step 1: Import `LucideAngularModule`
|
||||
|
||||
In any Angular module you wish to use Lucide icons in, you have to import `LucideAngularModule`, and pick any icons you wish to use:
|
||||
|
||||
```js
|
||||
import { LucideAngularModule, File, Home, Menu, UserCheck } from 'lucide-angular';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
LucideAngularModule.pick({File, Home, Menu, UserCheck})
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
||||
```
|
||||
|
||||
### Step 2: Use the icons in templates
|
||||
|
||||
Within your templates you may now use one of the following component tags to insert an icon:
|
||||
|
||||
```html
|
||||
<lucide-angular name="file" class="my-icon"></lucide-angular>
|
||||
<lucide-icon name="home" class="my-icon"></lucide-icon>
|
||||
<i-lucide name="menu" class="my-icon"></i-lucide>
|
||||
<span-lucide name="user-check" class="my-icon"></span-lucide>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
You can pass additional props to adjust the icon appearance.
|
||||
|
||||
| name | type | default |
|
||||
| --------------------- | --------- | ------------ |
|
||||
| `size` | *number* | 24 |
|
||||
| `color` | *string* | currentColor |
|
||||
| `strokeWidth` | *number* | 2 |
|
||||
| `absoluteStrokeWidth` | *boolean* | false |
|
||||
|
||||
```html
|
||||
<i-lucide name="home" [size]="48" color="red" [strokeWidth]="1"></i-lucide>
|
||||
```
|
||||
|
||||
### Global configuration
|
||||
|
||||
You can inject the `LucideIconConfig` service in your root component to globally configure the default property values as defined above.
|
||||
|
||||
### Styling using a custom CSS class
|
||||
|
||||
Any extra HTML attribute is ignored, but the `class` attribute
|
||||
is passed onto the internal SVG image element and it can be used to style it:
|
||||
|
||||
```css
|
||||
svg.my-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
stroke-width: 3;
|
||||
}
|
||||
```
|
||||
|
||||
## Injecting multiple icon providers
|
||||
|
||||
You may provide additional icons using the `LUCIDE_ICONS` injection token,
|
||||
which accepts multiple providers of the interface `LucideIconsProviderInterface`
|
||||
with the utility class `LucideIconsProvider` available for easier usage:
|
||||
|
||||
```js
|
||||
import { LUCIDE_ICONS, LucideIconProvider } from 'lucide-angular';
|
||||
import { MyIcon } from './icons/my-icon';
|
||||
|
||||
const myIcons = {MyIcon};
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{provide: LUCIDE_ICONS, multi: true, useValue: new LucideIconProvider(myIcons)},
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
||||
```
|
||||
|
||||
To add custom icons, you will first need to convert them to an [svgson format](https://github.com/elrumordelaluz/svgson).
|
||||
|
||||
## Loading all icons
|
||||
|
||||
> :warning: You may also opt to import all icons if necessary using the following format but be aware that this will significantly increase your application build size.
|
||||
|
||||
```js
|
||||
import { icons } from 'lucide-angular';
|
||||
|
||||
...
|
||||
|
||||
LucideAngularModule.pick(icons)
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user