Compare commits

..

5 Commits

Author SHA1 Message Date
Eric Fennis
d1d57d3c85 Format code 2025-07-04 17:40:28 +02:00
Eric Fennis
701c2fb6b2 Delete lucide-vue 2024-01-23 18:24:03 +01:00
Eric Fennis
294ec9c727 Add simlinks 2024-01-23 17:46:05 +01:00
Eric Fennis
99c883e60a revert import change 2024-01-23 17:45:55 +01:00
Eric Fennis
9e68779d22 create new vue directory 2024-01-23 17:45:43 +01:00
2362 changed files with 41193 additions and 26654 deletions

View File

@@ -1,16 +0,0 @@
# Custom words
fullscreen
gamepad
gantt
kanban
pilcrow
squircle
strikethrough
touchpad
ungroup
pilcrow
# Brands
codepen
codesandbox
dribbble

View File

@@ -6,5 +6,6 @@ tests
node_modules
.eslintrc.js
docs/images
docs/**/examples/
docs/guide/basics/examples
docs/guide/advanced/examples
packages/lucide-react/dynamicIconImports.js

View File

@@ -1,4 +1,4 @@
const DEFAULT_ATTRS = require('./tools/build-icons/render/default-attrs.json');
const DEFAULT_ATTRS = require('./scripts/render/default-attrs.json');
module.exports = {
root: true,
@@ -15,9 +15,7 @@ module.exports = {
'no-use-before-define': 'off',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['**/*.test.js', '**/*.spec.js', '**/scripts/**'],
},
{ devDependencies: ['**/*.test.js', '**/*.spec.js', './scripts/**'] },
],
'import/extensions': [
'error',
@@ -45,14 +43,11 @@ module.exports = {
'@html-eslint/no-inline-styles': 'error',
'@html-eslint/require-attrs': [
'error',
...Object.entries(DEFAULT_ATTRS).map(([attr, value]) => ({
tag: 'svg',
attr,
value: String(value),
})),
...Object.entries(DEFAULT_ATTRS)
.map(([attr, value]) => ({ tag: 'svg', attr, value: String(value) }))
],
'@html-eslint/indent': ['error', 2],
'@html-eslint/no-multiple-empty-lines': ['error', { max: 0 }],
"@html-eslint/no-multiple-empty-lines": ["error", { "max": 0 }],
'@html-eslint/no-extra-spacing-attrs': [
'error',
{
@@ -69,7 +64,7 @@ module.exports = {
'@html-eslint/element-newline': 'error',
'@html-eslint/no-trailing-spaces': 'error',
'@html-eslint/quotes': 'error',
},
}
},
],
};

View File

@@ -35,16 +35,6 @@ body:
placeholder: e.g. 0.289.1
validations:
required: true
- type: checkboxes
id: can-reproduce-in-latest-version
attributes:
label: Can you reproduce this in the latest version?
description: i.e. after running `npm install lucide-react@latest`
options:
- label: 'Yes'
- label: 'No'
validations:
required: false
- type: checkboxes
id: browsers
attributes:
@@ -69,9 +59,6 @@ body:
- label: Windows
- label: Linux
- label: macOS
- label: ChromeOS
- label: iOS
- label: Android
- label: Other/not relevant
- type: textarea
id: description

View File

@@ -30,9 +30,6 @@ body:
- label: Windows
- label: Linux
- label: macOS
- label: ChromeOS
- label: iOS
- label: Android
- label: Other/not relevant
- type: textarea
id: description

View File

@@ -1,5 +1,5 @@
name: 'Build and Test'
description: 'Builds and test a package'
name: "Build and Test"
description: "Builds and test a package"
inputs:
name:
@@ -7,7 +7,7 @@ inputs:
required: true
runs:
using: 'composite'
using: "composite"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -1,5 +1,5 @@
name: 'Check icons'
description: 'Cross-checks icon and category references in JSON descriptors'
name: "Check icons"
description: "Cross-checks icon and category references in JSON descriptors"
inputs:
name:
@@ -7,7 +7,7 @@ inputs:
required: true
runs:
using: 'composite'
using: "composite"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

30
.github/labeler.yml vendored
View File

@@ -1,92 +1,92 @@
# For changed dependencies
📦 dependencies:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- pnpm-lock.yaml
# For changes in documentation
📖 documentation:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- docs/*.md
- docs/**/*.md
# For changes in the site, but not markdown files
🌍 site:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'docs/**'
# For changes in the metadata
🫧 metadata:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'icons/*.json'
- categories/*
# For changes or added icons
🎨 icon:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'icons/*.svg'
# For changes in the lucide package
🧳 lucide package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide/*'
# For changes in the lucide React package
⚛️ react package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-react/*'
# For changes in the lucide React Native package
⚛️ react native package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-react-native/*'
# For changes in the lucide vue packages
💎 vue package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-vue/*'
- 'packages/lucide-vue-next/*'
# For changes in the lucide angular package
🅰️ angular package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-angular/*'
# For changes in the lucide preact package
⚛️ preact package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-preact/*'
# For changes in the lucide svelte package
🧣 svelte package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-svelte/*'
# For changes in the lucide solid package
🪝 solid package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-solid/*'
# For changes in the lucide static package
🪨 static package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-static/*'
# For changes in the lucide flutter package
🏹 flutter package:
- changed-files:
- changed-files:
- any-glob-to-any-file:
- 'packages/lucide-flutter/*'

View File

@@ -47,7 +47,7 @@
- [ ] I've made sure that the icons look sharp on low DPI displays.
- [ ] I've made sure that the icons look consistent with the icon set in size, optical volume and density.
- [ ] I've made sure that the icons are visually centered.
- [ ] I've correctly optimized all icons to three points of precision.
- [ ] I've correctly optimized all icons to two points of precision.
## Before Submitting <!-- For every PR! -->
<!-- All of these requirements must be fulfilled. -->

View File

@@ -1,13 +1,11 @@
name: Close stale issues and PR
on:
schedule:
- cron: '45 1 * * *'
- cron: "45 1 * * *"
jobs:
stale:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/stale@v9
with:
@@ -16,5 +14,4 @@ jobs:
close-pr-message: This PR was closed because it has been stalled for 5 days with no activity.
close-pr-label: 🧶 stale
days-before-stale: 30
days-before-issue-stale: -1
days-before-close: -1

View File

@@ -1,6 +1,6 @@
name: 'Pull Request Labeler'
name: "Pull Request Labeler"
on:
- pull_request_target
- pull_request_target
jobs:
triage:

View File

@@ -8,7 +8,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide-angular:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -26,20 +26,5 @@ jobs:
- name: Build
run: pnpm --filter lucide-angular build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-angular test

View File

@@ -29,7 +29,7 @@ jobs:
- name: Create font in ./lucide-font
run: pnpm build:font
- name: 'Upload to Artifacts'
- name: "Upload to Artifacts"
uses: actions/upload-artifact@v3
with:
name: lucide-font

View File

@@ -24,5 +24,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter lucide-preact build
- name: Test
run: pnpm --filter lucide-preact test

View File

@@ -24,5 +24,8 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm --filter lucide-react-native build
- name: Test
run: pnpm --filter lucide-react-native test

View File

@@ -10,7 +10,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide-react:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -28,20 +28,5 @@ jobs:
- name: Build
run: pnpm --filter lucide-react build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-react test

View File

@@ -9,7 +9,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide-solid:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -27,20 +27,5 @@ jobs:
- name: Build
run: pnpm --filter lucide-solid build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-solid test

View File

@@ -9,7 +9,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide-svelte:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -27,20 +27,5 @@ jobs:
- name: Build
run: pnpm --filter lucide-svelte build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-svelte test

View File

@@ -9,7 +9,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide-vue-next:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -27,20 +27,5 @@ jobs:
- name: Build
run: pnpm --filter lucide-vue-next build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-vue-next test

View File

@@ -9,7 +9,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide-vue:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -27,20 +27,5 @@ jobs:
- name: Build
run: pnpm --filter lucide-vue build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide-vue test

View File

@@ -9,7 +9,7 @@ on:
- pnpm-lock.yaml
jobs:
build:
lucide:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -27,20 +27,5 @@ jobs:
- name: Build
run: pnpm --filter lucide build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3.8.1
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm --filter lucide test

View File

@@ -1,16 +0,0 @@
name: Validate PR title
on:
pull_request:
types:
- opened
- edited
- synchronize
jobs:
semantic-pull-request:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -3,7 +3,7 @@ name: Add Changed Icons comment
on:
pull_request_target:
paths:
- 'icons/*'
- 'icons/*.svg'
branches:
- main
- fix-icon-preview
@@ -68,16 +68,6 @@ jobs:
# input: +++ b/icons/accessibility.json%0A@@ -2,0 +3 @@%0A+ "contributors": ["hi"],%0A@@ -13 +14 @@%0A+}%0A
# output: ::$ANNOTATION_SEVERITY file=icons/accessibility.json,line=2,endLine=3,title=$ANNOTATION_TITLE::$ANNOTATION_DESCRIPTION%0A%0A+ "contributors": ["hi"],%0A@@ -13 +14 @@%0A+}%0A
lint-aliases:
name: Check Uniqueness of Aliases
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Check Uniqueness of Aliases
run: "! cat <(printf \"%s\\n\" icons/*.json | while read -r name; do basename \"$name\" .json; done) <(jq -cr 'select(.aliases) | .aliases[] | if type==\"string\" then . else .name end' icons/*.json) | sort | uniq -c | grep -ve '^\\s*1 '"
generate-changed-icons-comment:
runs-on: ubuntu-latest
permissions:
@@ -104,10 +94,6 @@ jobs:
comment-author: 'github-actions[bot]'
body-includes: Added or changed icons
- uses: actions/setup-node@v4
- name: Install svgson for code preview (safer and faster than installing all deps)
run: npm install svgson
- name: Generate comment markup
run: node ./scripts/generateChangedIconsCommentMarkup.mjs >> comment-markup.md
id: comment-markup

View File

@@ -41,8 +41,7 @@ jobs:
strategy:
fail-fast: false
matrix:
package:
[
package: [
'lucide',
'lucide-react',
'lucide-react-native',
@@ -137,7 +136,7 @@ jobs:
- name: Create font in ./lucide-font
run: pnpm build:font
- name: 'Upload to Artifacts'
- name: "Upload to Artifacts"
uses: actions/upload-artifact@v3
with:
name: lucide-font
@@ -146,7 +145,10 @@ jobs:
post-release:
if: github.repository == 'lucide-icons/lucide'
runs-on: ubuntu-latest
needs: [pre-release, lucide-font]
needs: [
pre-release,
lucide-font,
]
steps:
- uses: actions/checkout@v4

1
.gitignore vendored
View File

@@ -34,7 +34,6 @@ docs/.vitepress/data/iconNodes
docs/.vitepress/data/iconMetaData.ts
docs/.vitepress/data/releaseMetaData.json
docs/.vitepress/data/releaseMetaData
docs/.vitepress/data/categoriesData.json
docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vercel

View File

@@ -1,12 +1,5 @@
pnpm-lock.yaml
# docs examples
docs/**/examples/
docs/.vitepress/.temp
docs/.vitepress/cache
docs/.vitepress/data
docs/.nitro
# lucide-angular
packages/lucide-angular/.angular/cache

11
.vscode/settings.json vendored
View File

@@ -1,6 +1,13 @@
{
"cSpell.words": ["devs", "preact", "Preact"],
"cSpell.words": [
"devs",
"preact",
"Preact"
],
"eslint.enable": true,
"eslint.validate": ["javascript", "svg"],
"eslint.validate": [
"javascript",
"svg"
],
"svg.preview.background": "transparent"
}

View File

@@ -49,7 +49,7 @@
"circle",
"<circle"
],
"body": "<circle cx=\"${2:12}\" cy=\"${3:$2}\" r=\"${1|10,2,.5\" fill=\"currentColor|}\" />"
"body": "<circle cx=\"${2:12}\" cy=\"${3:$2}\" r=\"${1|10,2,.5|}\" />"
},
"Ellipse": {
"scope": "xml",

View File

@@ -39,8 +39,6 @@ You can also [download an Adobe Illustrator template](https://github.com/lucide-
#### [Figma Guide](https://lucide.dev/docs/figma-guide)
#### [Affinity Designer Guide](https://lucide.dev/guide/design/affinity-designer-guide)
### Submitting Multiple Icons
If you want submit multiple icons, please separate the icons and group them. That makes reviewing the icons easier and keep the thread clean and scoped.

View File

@@ -1,11 +1,4 @@
<p align="center">
<a href="https://github.com/lucide-icons/lucide#gh-light-mode-only">
<img src="https://lucide.dev/lucide-logo-repo.svg#gh-light-mode-only" alt="Lucide - Beautiful & consistent icon toolkit made by the community. Open-source project and a fork of Feather Icons." width="480">
</a>
<a href="https://github.com/lucide-icons/lucide#gh-dark-mode-only">
<img src="https://lucide.dev/lucide-logo-repo-dark.svg#gh-dark-mode-only" alt="Lucide - Beautiful & consistent icon toolkit made by the community. Open-source project and a fork of Feather Icons." width="480">
</a>
</p>
<p align=center><img width="480" src="https://lucide.dev/lucide-logo-repo.svg" alt="Lucide Logo"></p>
<p align="center">
<a href="https://github.com/lucide-icons/lucide/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/lucide" alt="license"></a>
<a href="https://www.npmjs.com/package/lucide"><img src="https://img.shields.io/npm/v/lucide" alt="npm package"></a>
@@ -281,12 +274,9 @@ Thank you to all the people who contributed to Lucide!
## Sponsors
<a href="https://vercel.com?utm_source=lucide&utm_campaign=oss">
<img src="docs/public/vercel.svg" alt="Powered by Vercel" width="200" />
</a>
<a href="https://www.digitalocean.com/?refcode=b0877a2caebd&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"><img src="docs/public/digitalocean.svg" width="200" alt="DigitalOcean Referral Badge" /></a>
### Awesome backer 🍺
<a href="https://www.scipress.io?utm_source=lucide"><img src="docs/public/sponsors/scipress.svg" width="180" alt="Scipress sponsor badge" /></a>

View File

@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Coding & development",
"icon": "code-xml"
"icon": "code-2"
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Home",
"icon": "house"
"icon": "home"
}

View File

@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Multimedia",
"icon": "circle-play"
"icon": "play-circle"
}

View File

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

View File

@@ -1,28 +0,0 @@
import type { UserConfig } from '@commitlint/types';
import fs from 'fs/promises'
import path from 'path'
// Read directory
const getAllPackageFromDirectory = async (directory: string) => {
return fs.readdir(path.resolve(process.cwd(), directory))
}
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-enum': async () => {
const packages = await getAllPackageFromDirectory('packages')
const toolPackages = await getAllPackageFromDirectory('tools')
return [2, 'always', [
'site',
'meta',
...packages,
...toolPackages,
]
]
}
},
};
export default Configuration;

View File

@@ -1,10 +0,0 @@
{
"dictionaries": ["en-us", "custom-words"],
"dictionaryDefinitions": [
{
"name": "custom-words",
"path": "./.cspell/custom-words.txt",
"addWords": true
}
]
}

View File

@@ -1,10 +1,11 @@
import iconMetaData from '../../data/iconMetaData';
import { eventHandler, setResponseHeader } from 'h3'
import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { categories }]) => [name, categories]),
);
});
Object.entries(iconMetaData).map(([name, { categories }]) => [ name, categories ])
)
})

View File

@@ -1,40 +0,0 @@
import iconNodes from '../../data/iconNodes/index.ts';
import { IconNodeWithKeys } from '../../theme/types';
import iconMetaData from '../../data/iconMetaData';
import releaseMeta from '../../data/releaseMetaData.json';
import categories from '../../data/categoriesData.json';
const dataResponse = {
icons: Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
return [name, attrs];
});
acc[name] = {
iconNode: newIconNode,
aliases: (iconMetaData[name]?.aliases ?? []).map((alias) =>
typeof alias === 'string' ? alias : alias.name,
),
tags: iconMetaData[name].tags ?? [],
categories: iconMetaData[name].categories ?? [],
...releaseMeta[name],
};
return acc;
}, {}),
aliases: Object.entries(iconNodes).reduce((acc, [name]) => {
for (const alias of iconMetaData[name]?.aliases ?? []) {
acc[typeof alias === 'string' ? alias : alias.name] = name;
}
return acc;
}, {}),
categories,
};
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
return dataResponse;
});

View File

@@ -13,10 +13,7 @@ export default eventHandler((event) => {
const data = pathData.at(-1).slice(0, -4);
const [name] = pathData;
const src = Buffer.from(data, 'base64')
.toString('utf8')
.replaceAll('\n', '')
.replace(/<svg[^>]*>|<\/svg>/g, '');
const src = Buffer.from(data, 'base64').toString('utf8');
const children = [];
@@ -33,29 +30,20 @@ export default eventHandler((event) => {
const LucideIcon = createLucideIcon(backdropName, iconNode);
const svg = renderToStaticMarkup(createElement(LucideIcon));
const backdropString = svg.replaceAll('\n', '').replace(/<svg[^>]*>|<\/svg>/g, '');
const backdropString = svg.replace(/<svg[^>]*>|<\/svg>/g, '');
children.push(
createElement(Backdrop, {
backdropString,
src,
color: name in iconNodes ? 'red' : '#777',
}),
})
);
if (name in iconNodes) {
children.push(
createElement(Backdrop, {
backdropString: src,
src: backdropString,
color: 'lime',
}),
);
}
}
const svg = Buffer.from(
// We can't use jsx here, is not supported here by nitro.
renderToString(createElement(SvgPreview, { src, showGrid: true }, children)),
renderToString(createElement(SvgPreview, { src, showGrid: true }, children))
).toString('utf8');
defaultContentType(event, 'image/svg+xml');

View File

@@ -28,7 +28,7 @@ export default eventHandler(async (event) => {
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
`,
`
);
const resvg = new Resvg(svg, { background: '#000' });

View File

@@ -1,12 +1,12 @@
import { eventHandler, setResponseHeader, defaultContentType } from 'h3';
import { renderToString } from 'react-dom/server';
import { createElement } from 'react';
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 createLucideIcon, { IconNode } from 'lucide-react/src/createLucideIcon'
import { parseSync } from 'svgson';
export default eventHandler((event) => {
const { params } = event.context;
const { params } = event.context
const [strokeWidth, svgData] = params.data.split('/');
const data = svgData.slice(0, -4);
@@ -16,8 +16,8 @@ export default eventHandler((event) => {
const Icon = createLucideIcon(
'icon',
parseSync(src.includes('<svg') ? src : `<svg>${src}</svg>`).children.map(
({ name, attributes }) => [name, attributes],
) as IconNode,
({ name, attributes }) => [name, attributes]
) as IconNode
);
const svg = Buffer.from(
@@ -33,12 +33,12 @@ export default eventHandler((event) => {
@media screen and (prefers-color-scheme: dark) {
svg { stroke: #fff; fill: transparent !important; }
}
</style>`,
),
</style>`
)
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
return svg;
});
return svg
})

View File

@@ -1,30 +1,30 @@
import { eventHandler, getQuery, setResponseHeader } from 'h3';
import iconNodes from '../../data/iconNodes';
import { IconNodeWithKeys } from '../../theme/types';
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 query = getQuery(event)
const withUniqueKeys = query.withUniqueKeys === 'true';
const withUniqueKeys = query.withUniqueKeys === 'true'
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
if (withUniqueKeys) {
return iconNodes;
return iconNodes
}
return Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
if (withUniqueKeys) {
return [name, iconNode];
return [name, iconNode]
}
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
return [name, attrs];
});
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs}]) => {
return [name, attrs]
})
acc[name] = newIconNode;
acc[name] = newIconNode
return acc;
}, {});
});
return acc
}, {})
})

View File

@@ -1,29 +1,29 @@
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';
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 { params } = event.context
const iconNode = iconNodes[params.iconName];
const iconNode = iconNodes[params.iconName]
if (iconNode == null) {
const error = createError({
statusCode: 404,
message: `Icon "${params.iconName}" not found`,
});
})
return sendError(event, error);
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 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 LucideIcon = createLucideIcon(params.iconName, iconNode)
const svg = Buffer.from(
renderToString(
@@ -32,13 +32,14 @@ export default eventHandler((event) => {
height,
color: color ? `#${color}` : undefined,
strokeWidth,
}),
),
}
))
).toString('utf8');
defaultContentType(event, 'image/svg+xml');
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
defaultContentType(event, 'image/svg+xml')
setResponseHeader(event, 'Cache-Control', 'public,max-age=31536000')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return svg;
});
return svg
})

View File

@@ -1,8 +1,11 @@
import iconMetaData from '../../data/iconMetaData';
import { eventHandler, setResponseHeader } from 'h3'
import iconMetaData from '../../data/iconMetaData'
export default eventHandler((event) => {
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400');
setResponseHeader(event, 'Access-Control-Allow-Origin', '*');
setResponseHeader(event, 'Cache-Control', 'public, max-age=86400')
setResponseHeader(event, 'Access-Control-Allow-Origin', '*')
return Object.fromEntries(Object.entries(iconMetaData).map(([name, { tags }]) => [name, tags]));
});
return Object.fromEntries(
Object.entries(iconMetaData).map(([name, { tags }]) => [ name, tags ])
)
})

View File

@@ -1,3 +1,3 @@
export default eventHandler(() => {
return { nitro: 'Is Awesome! asda' };
});
return { nitro: 'Is Awesome! asda' }
})

View File

@@ -1,10 +1,10 @@
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vitepress';
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitepress'
import sidebar from './sidebar';
const title = 'Lucide';
const socialTitle = 'Lucide Icons';
const description = 'Beautiful & consistent icon toolkit made by the community.';
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({
@@ -19,135 +19,86 @@ export default defineConfig({
{
find: /^.*\/VPIconAlignLeft\.vue$/,
replacement: fileURLToPath(
new URL('./theme/components/overrides/VPIconAlignLeft.vue', import.meta.url),
),
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),
),
},
{
find: '~/.vitepress',
replacement: fileURLToPath(new URL('./', import.meta.url)),
},
],
new URL('./theme/components/overrides/VPFooter.vue', import.meta.url)
)
}
]
},
},
head: [
[
'script',
{
[ 'script', {
src: 'https://analytics.lucide.dev/js/script.js',
'data-domain': 'lucide.dev',
defer: '',
},
],
[
'meta',
{
property: 'og:locale',
content: 'en_US',
},
],
[
'meta',
{
property: 'og:type',
content: 'website',
},
],
[
'meta',
{
property: 'og:site_name',
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',
}],
[ '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',
}],
[ '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',
},
],
}],
[ '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',
dark: '/logo.dark.svg'
},
nav: [
{ text: 'Icons', link: '/icons/' },
@@ -159,21 +110,21 @@ export default defineConfig({
sidebar,
socialLinks: [
{ icon: 'github', link: 'https://github.com/lucide-icons/lucide' },
{ icon: 'discord', link: 'https://discord.gg/EH6nSts' },
{ icon: 'discord', link: 'https://discord.gg/EH6nSts' }
],
footer: {
message: 'Released under the ISC License.',
copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`,
copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`
},
editLink: {
pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path',
pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path'
},
carbonAds: {
code: 'CWYIC53U',
placement: 'lucidedev',
},
placement: 'lucidedev'
}
},
sitemap: {
hostname: 'https://lucide.dev/',
},
});
hostname: 'https://lucide.dev/'
}
})

View File

@@ -1,186 +0,0 @@
[
{
"name": "accessibility",
"title": "Accessibility"
},
{
"name": "account",
"title": "Accounts & access"
},
{
"name": "animals",
"title": "Animals"
},
{
"name": "arrows",
"title": "Arrows"
},
{
"name": "brands",
"title": "Brands"
},
{
"name": "buildings",
"title": "Buildings"
},
{
"name": "charts",
"title": "Charts"
},
{
"name": "communication",
"title": "Communication"
},
{
"name": "connectivity",
"title": "Connectivity"
},
{
"name": "currency",
"title": "Currency"
},
{
"name": "cursors",
"title": "Cursors"
},
{
"name": "design",
"title": "Design"
},
{
"name": "development",
"title": "Coding & development"
},
{
"name": "devices",
"title": "Devices"
},
{
"name": "emoji",
"title": "Emoji"
},
{
"name": "files",
"title": "File icons"
},
{
"name": "food-beverage",
"title": "Food & beverage"
},
{
"name": "furniture",
"title": "Furniture"
},
{
"name": "gaming",
"title": "Gaming"
},
{
"name": "home",
"title": "Home"
},
{
"name": "layout",
"title": "Layout"
},
{
"name": "mail",
"title": "Mail"
},
{
"name": "maps",
"title": "Maps"
},
{
"name": "maths",
"title": "Maths"
},
{
"name": "medical",
"title": "Medical"
},
{
"name": "money",
"title": "Money"
},
{
"name": "multimedia",
"title": "Multimedia"
},
{
"name": "nature",
"title": "Nature"
},
{
"name": "navigation",
"title": "Navigation"
},
{
"name": "notifications",
"title": "Notifications"
},
{
"name": "people",
"title": "People"
},
{
"name": "photography",
"title": "Photography"
},
{
"name": "science",
"title": "Science"
},
{
"name": "seasons",
"title": "Seasons"
},
{
"name": "security",
"title": "Security"
},
{
"name": "shapes",
"title": "Shapes"
},
{
"name": "shopping",
"title": "Shopping"
},
{
"name": "social",
"title": "Social"
},
{
"name": "sports",
"title": "Sports"
},
{
"name": "sustainability",
"title": "Sustainability"
},
{
"name": "text",
"title": "Text formatting"
},
{
"name": "time",
"title": "Time & calendar"
},
{
"name": "tools",
"title": "Tools"
},
{
"name": "transportation",
"title": "Transportation"
},
{
"name": "travel",
"title": "Travel"
},
{
"name": "weather",
"title": "Weather"
}
]

View File

@@ -3,160 +3,87 @@
"order": 0,
"icon": "js",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide",
"href": "https://www.npmjs.com/package/lucide"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide",
"href": "https://www.npmjs.com/package/lucide"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide", "href": "https://www.npmjs.com/package/lucide" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide", "href": "https://www.npmjs.com/package/lucide" }
]
},
"lucide-react": {
"order": 1,
"icon": "react",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-react",
"href": "https://www.npmjs.com/package/lucide-react"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-react",
"href": "https://www.npmjs.com/package/lucide-react"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-react", "href": "https://www.npmjs.com/package/lucide-react" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-react", "href": "https://www.npmjs.com/package/lucide-react" }
]
},
"lucide-vue": {
"order": 2,
"icon": "vue",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-vue",
"href": "https://www.npmjs.com/package/lucide-vue"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-vue",
"href": "https://www.npmjs.com/package/lucide-vue"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-vue", "href": "https://www.npmjs.com/package/lucide-vue" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-vue", "href": "https://www.npmjs.com/package/lucide-vue" }
]
},
"lucide-vue-next": {
"order": 3,
"icon": "vue-next",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-vue-next",
"href": "https://www.npmjs.com/package/lucide-vue-next"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-vue-next",
"href": "https://www.npmjs.com/package/lucide-vue-next"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-vue-next", "href": "https://www.npmjs.com/package/lucide-vue-next" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-vue-next", "href": "https://www.npmjs.com/package/lucide-vue-next" }
]
},
"lucide-svelte": {
"order": 4,
"icon": "svelte",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-svelte",
"href": "https://www.npmjs.com/package/lucide-svelte"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-svelte",
"href": "https://www.npmjs.com/package/lucide-svelte"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-svelte", "href": "https://www.npmjs.com/package/lucide-svelte" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-svelte", "href": "https://www.npmjs.com/package/lucide-svelte" }
]
},
"lucide-solid": {
"order": 4,
"icon": "solid",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-solid",
"href": "https://www.npmjs.com/package/lucide-solid"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-solid",
"href": "https://www.npmjs.com/package/lucide-solid"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-solid", "href": "https://www.npmjs.com/package/lucide-solid" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-solid", "href": "https://www.npmjs.com/package/lucide-solid" }
]
},
"lucide-preact": {
"order": 5,
"icon": "preact",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-preact",
"href": "https://www.npmjs.com/package/lucide-preact"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-preact",
"href": "https://www.npmjs.com/package/lucide-preact"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-preact", "href": "https://www.npmjs.com/package/lucide-preact" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-preact", "href": "https://www.npmjs.com/package/lucide-preact" }
]
},
"lucide-react-native": {
"order": 6,
"icon": "react-native",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-react-native",
"href": "https://www.npmjs.com/package/lucide-react-native"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-react-native",
"href": "https://www.npmjs.com/package/lucide-react-native"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-react-native", "href": "https://www.npmjs.com/package/lucide-react-native" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-react-native", "href": "https://www.npmjs.com/package/lucide-react-native" }
]
},
"lucide-angular": {
"order": 7,
"icon": "angular",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-angular",
"href": "https://www.npmjs.com/package/lucide-angular"
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-angular",
"href": "https://www.npmjs.com/package/lucide-angular"
}
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-angular", "href": "https://www.npmjs.com/package/lucide-angular" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-angular", "href": "https://www.npmjs.com/package/lucide-angular" }
]
},
"lucide-static": {
"order": 8,
"icon": "svg",
"shields": [
{
"alt": "npm",
"src": "https://img.shields.io/npm/v/lucide-static",
"href": "https://www.npmjs.com/package/lucide-static"
{ "alt": "npm", "src": "https://img.shields.io/npm/v/lucide-static", "href": "https://www.npmjs.com/package/lucide-static" },
{ "alt": "npm", "src": "https://img.shields.io/npm/dw/lucide-static", "href": "https://www.npmjs.com/package/lucide-static" }
]
},
{
"alt": "npm",
"src": "https://img.shields.io/npm/dw/lucide-static",
"href": "https://www.npmjs.com/package/lucide-static"
}
"lucide-flutter": {
"order": 9,
"icon": "flutter",
"shields": [
{ "alt": "flutter", "src": "https://img.shields.io/pub/v/lucide_icons", "href": "https://img.shields.io/pub/v/lucide_icons" }
]
}
}

View File

@@ -1,48 +0,0 @@
[
{
"name": "Eric Fennis",
"title": "Creator of Lucide & Software engineer @nedap",
"image": "https://github.com/ericfennis.png?size=192",
"sponsor": "https://github.com/sponsors/ericfennis",
"socialLinks": [
{
"icon": "github",
"link": "https://github.com/ericfennis"
},
{
"icon": "x",
"link": "https://github.com/ericfennis"
}
]
},
{
"name": "Karsa Rigó",
"title": "Maintainer of Lucide & Software engineer @sztaki",
"image": "https://github.com/karsa-mistmere.png?size=192",
"socialLinks": [
{
"icon": "github",
"link": "https://github.com/karsa-mistmere"
},
{
"icon": "linkedin",
"link": "https://www.linkedin.com/in/karsamistmere"
}
]
},
{
"name": "Jakob Guddas",
"title": "Maintainer of Lucide & Software engineer @LEGO",
"image": "https://github.com/jguddas.png?size=192",
"socialLinks": [
{
"icon": "github",
"link": "https://github.com/jguddas"
},
{
"icon": "linkedin",
"link": "https://www.linkedin.com/in/jguddas"
}
]
}
]

View File

@@ -7,80 +7,51 @@ interface BackdropProps {
}
const Backdrop = ({ src, color = 'red', backdropString }: BackdropProps): JSX.Element => {
const id = React.useId();
return (
<>
<defs xmlns="http://www.w3.org/2000/svg">
<pattern
id={`pattern-${id}`}
id="pattern"
width=".1"
height=".1"
patternUnits="userSpaceOnUse"
patternTransform="rotate(45 50 50)"
>
<line
stroke={color}
strokeWidth={0.1}
y2={1}
/>
<line
stroke={color}
strokeWidth={0.1}
y2={1}
/>
<line stroke={color} strokeWidth={0.1} y2={1} />
<line stroke={color} strokeWidth={0.1} y2={1} />
</pattern>
</defs>
<mask
id={`svg-preview-backdrop-mask-${id}`}
maskUnits="userSpaceOnUse"
>
<g
stroke="#fff"
dangerouslySetInnerHTML={{ __html: backdropString }}
/>
<g dangerouslySetInnerHTML={{ __html: src }} />
<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-outline-${id}`}
maskUnits="userSpaceOnUse"
>
<rect
x="0"
y="0"
width="24"
height="24"
fill="#fff"
stroke="none"
/>
<g
strokeWidth={1.75}
dangerouslySetInnerHTML={{ __html: backdropString }}
/>
<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 mask={`url(#svg-preview-backdrop-mask-${id})`}>
<rect
x="0"
y="0"
width="24"
height="24"
opacity={0.5}
fill={`url(#pattern-${id})`}
stroke="none"
/>
<g
stroke={color}
strokeWidth={2.25}
opacity={0.75}
dangerouslySetInnerHTML={{ __html: src }}
/>
<g
stroke={color}
strokeWidth={2.25}
opacity={0.75}
mask={`url(#svg-preview-backdrop-mask-outline-${id})`}
dangerouslySetInnerHTML={{ __html: backdropString }}
/>
<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={color}
opacity={0.5}
stroke="none"
mask={'url(#svg-preview-backdrop-mask-fill)'}
/>
</>
);
};

View File

@@ -10,11 +10,7 @@ const Grid = ({
strokeWidth: number;
radius: number;
} & PathProps<'stroke', 'strokeWidth'>) => (
<g
className="svg-preview-grid-group"
strokeLinecap="butt"
{...props}
>
<g className="svg-preview-grid-group" strokeLinecap="butt" {...props}>
<rect
className="svg-preview-grid-rect"
width={24 - props.strokeWidth}
@@ -25,29 +21,11 @@ const Grid = ({
fill={fill}
/>
<path
strokeDasharray={'0 0.1 ' + '0.1 0.15 '.repeat(11) + '0 0.15'}
strokeWidth={0.1}
d={
props.d ||
new Array(Math.floor(24 - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => i % 3 !== 2)
.flatMap((i) => [
`M${props.strokeWidth} ${i + 1}h${24 - props.strokeWidth * 2}`,
`M${i + 1} ${props.strokeWidth}v${24 - props.strokeWidth * 2}`,
])
.join('')
}
/>
<path
d={
props.d ||
new Array(Math.floor(24 - 1))
.fill(null)
.map((_, i) => i)
.filter((i) => i % 3 === 2)
.flatMap((i) => [
.flatMap((_, i) => [
`M${props.strokeWidth} ${i + 1}h${24 - props.strokeWidth * 2}`,
`M${i + 1} ${props.strokeWidth}v${24 - props.strokeWidth * 2}`,
])
@@ -66,21 +44,15 @@ const Shadow = ({
paths: Path[];
} & PathProps<'stroke' | 'strokeWidth' | 'strokeOpacity', 'd'>) => {
const groupedPaths = Object.entries(
paths.reduce(
(groups, val) => {
paths.reduce((groups, val) => {
const key = val.c.id;
groups[key] = [...(groups[key] || []), val];
return groups;
},
{} as Record<number, Path[]>,
),
}, {} as Record<number, Path[]>)
);
return (
<>
<g
className="svg-preview-shadow-mask-group"
{...props}
>
<g className="svg-preview-shadow-mask-group" {...props}>
{groupedPaths.map(([id, paths]) => (
<mask
id={`svg-preview-shadow-mask-${id}`}
@@ -89,15 +61,7 @@ const Shadow = ({
strokeWidth={props.strokeWidth}
stroke="#000"
>
<rect
x={0}
y={0}
width={24}
height={24}
fill="#fff"
stroke="none"
rx={radius}
/>
<rect x={0} y={0} width={24} height={24} fill="#fff" stroke="none" rx={radius} />
<path
d={paths
.flatMap(({ prev, next }) => [
@@ -110,16 +74,9 @@ const Shadow = ({
</mask>
))}
</g>
<g
className="svg-preview-shadow-group"
{...props}
>
<g className="svg-preview-shadow-group" {...props}>
{paths.map(({ d, c: { id } }, i) => (
<path
key={i}
mask={`url(#svg-preview-shadow-mask-${id})`}
d={d}
/>
<path key={i} mask={`url(#svg-preview-shadow-mask-${id})`} d={d} />
))}
<path
d={paths
@@ -137,16 +94,9 @@ const ColoredPath = ({
paths,
...props
}: { paths: Path[]; colors: string[] } & PathProps<never, 'd' | 'stroke'>) => (
<g
className="svg-preview-colored-path-group"
{...props}
>
<g className="svg-preview-colored-path-group" {...props}>
{paths.map(({ d, c }, i) => (
<path
key={i}
d={d}
stroke={colors[(c.name === 'path' ? i : c.id) % colors.length]}
/>
<path key={i} d={d} stroke={colors[(c.name === 'path' ? i : c.id) % colors.length]} />
))}
</g>
);
@@ -188,15 +138,7 @@ const ControlPath = ({
key={i}
maskUnits="userSpaceOnUse"
>
<rect
x="0"
y="0"
width="24"
height="24"
fill="#fff"
stroke="none"
rx={radius}
/>
<rect x="0" y="0" width="24" height="24" fill="#fff" stroke="none" rx={radius} />
<path d={`M${prev.x} ${prev.y}h.01`} />
<path d={`M${next.x} ${next.y}h.01`} />
</mask>
@@ -204,10 +146,7 @@ const ControlPath = ({
);
})}
</g>
<g
className="svg-preview-control-path-group"
{...props}
>
<g className="svg-preview-control-path-group" {...props}>
{controlPaths.map(({ d, showMarker }, i) => (
<path
key={i}
@@ -216,33 +155,18 @@ const ControlPath = ({
/>
))}
</g>
<g
className="svg-preview-control-path-marker-group"
{...props}
>
<g className="svg-preview-control-path-marker-group" {...props}>
<path
d={controlPaths
.flatMap(({ prev, next, showMarker }) =>
showMarker ? [`M${prev.x} ${prev.y}h.01`, `M${next.x} ${next.y}h.01`] : [],
showMarker ? [`M${prev.x} ${prev.y}h.01`, `M${next.x} ${next.y}h.01`] : []
)
.join('')}
/>
{controlPaths.map(({ d, prev, next, startMarker, endMarker }, i) => (
<React.Fragment key={i}>
{startMarker && (
<circle
cx={prev.x}
cy={prev.y}
r={pointSize / 2}
/>
)}
{endMarker && (
<circle
cx={next.x}
cy={next.y}
r={pointSize / 2}
/>
)}
{startMarker && <circle cx={prev.x} cy={prev.y} r={pointSize / 2} />}
{endMarker && <circle cx={next.x} cy={next.y} r={pointSize / 2} />}
</React.Fragment>
))}
</g>
@@ -258,16 +182,15 @@ const Radii = ({
any
>) => {
return (
<g
className="svg-preview-radii-group"
{...props}
>
<g className="svg-preview-radii-group" {...props}>
{paths.map(
({ c, prev, next, circle }, i) =>
circle && (
<React.Fragment key={i}>
{c.name !== 'circle' && (
<path d={`M${prev.x} ${prev.y} ${circle.x} ${circle.y} ${next.x} ${next.y}`} />
{c.name !== "circle" && (
<path
d={`M${prev.x} ${prev.y} ${circle.x} ${circle.y} ${next.x} ${next.y}`}
/>
)}
<circle
cy={circle.y}
@@ -277,7 +200,7 @@ const Radii = ({
stroke={
(Math.round(circle.x * 100) / 100) % 1 !== 0 ||
(Math.round(circle.y * 100) / 100) % 1 !== 0
? 'red'
? "red"
: undefined
}
/>
@@ -285,7 +208,11 @@ const Radii = ({
cy={circle.y}
cx={circle.x}
r={circle.r}
stroke={(Math.round(circle.r * 1000) / 1000) % 1 !== 0 ? 'red' : undefined}
stroke={
(Math.round(circle.r * 1000) / 1000) % 1 !== 0
? "red"
: undefined
}
/>
</React.Fragment>
),
@@ -301,29 +228,15 @@ const Handles = ({
'strokeWidth' | 'stroke' | 'strokeDasharray' | 'strokeOpacity',
any
>) => {
console.log(paths);
return (
<g
className="svg-preview-handles-group"
{...props}
>
<g className="svg-preview-handles-group" {...props}>
{paths.map(({ c, prev, next, cp1, cp2 }) => (
<>
{cp1 && <path d={`M${prev.x} ${prev.y} ${cp1.x} ${cp1.y}`} />}
{cp1 && (
<circle
cy={cp1.y}
cx={cp1.x}
r={0.25}
/>
)}
{cp1 && <circle cy={cp1.y} cx={cp1.x} r={0.25} />}
{cp2 && <path d={`M${next.x} ${next.y} ${cp2.x} ${cp2.y}`} />}
{cp2 && (
<circle
cy={cp2.y}
cx={cp2.x}
r={0.25}
/>
)}
{cp2 && <circle cy={cp2.y} cx={cp2.x} r={0.25} />}
</>
))}
</g>
@@ -367,27 +280,9 @@ const SvgPreview = React.forwardRef<
{...props}
>
<style>{darkModeCss}</style>
{showGrid && (
<Grid
strokeWidth={0.1}
stroke="#777"
strokeOpacity={0.3}
radius={1}
/>
)}
<Shadow
paths={paths}
strokeWidth={4}
stroke="#777"
radius={1}
strokeOpacity={0.15}
/>
<Handles
paths={paths}
strokeWidth={0.12}
stroke="#777"
strokeOpacity={0.6}
/>
{showGrid && <Grid strokeWidth={0.1} stroke="#777" strokeOpacity={0.3} radius={1} />}
<Shadow paths={paths} strokeWidth={4} stroke="#777" radius={1} strokeOpacity={0.15} />
<Handles paths={paths} strokeWidth={0.12} stroke="#777" strokeOpacity={0.6} />
<ColoredPath
paths={paths}
colors={[
@@ -412,19 +307,8 @@ const SvgPreview = React.forwardRef<
stroke="#777"
strokeOpacity={0.3}
/>
<ControlPath
radius={1}
paths={paths}
pointSize={1}
stroke="#fff"
strokeWidth={0.125}
/>
<Handles
paths={paths}
strokeWidth={0.12}
stroke="#FFF"
strokeOpacity={0.3}
/>
<ControlPath radius={1} paths={paths} pointSize={1} stroke="#fff" strokeWidth={0.125} />
<Handles paths={paths} strokeWidth={0.12} stroke="#FFF" strokeOpacity={0.3} />
{children}
</svg>
);

View File

@@ -16,7 +16,7 @@ export type Path = {
export type PathProps<
RequiredProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
NeverProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
NeverProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>
> = Required<Pick<React.SVGProps<SVGElement & SVGRectElement & SVGCircleElement>, RequiredProps>> &
Omit<
React.SVGProps<SVGPathElement & SVGRectElement & SVGCircleElement>,

View File

@@ -51,7 +51,7 @@ export const getCommands = (src: string) =>
getNodes(src)
.map(convertToPathNode)
.flatMap(({ d, name }, idx) =>
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name }))
);
export const getPaths = (src: string) => {
@@ -60,10 +60,10 @@ export const getPaths = (src: string) => {
let prev: Point | undefined = undefined;
let start: Point | undefined = undefined;
const addPath = (
c: (typeof commands)[number],
c: typeof commands[number],
next: Point,
d?: string,
extras?: { circle?: Path['circle']; cp1?: Path['cp1']; cp2?: Path['cp2'] },
extras?: { circle?: Path['circle']; cp1?: Path['cp1']; cp2?: Path['cp2'] }
) => {
assert(prev);
paths.push({
@@ -153,7 +153,7 @@ export const getPaths = (src: string) => {
{
cp1: { x: prev.x - reflectedCp1.x, y: prev.y - reflectedCp1.y },
cp2: { x: c.x2, y: c.y2 },
},
}
);
break;
}
@@ -169,7 +169,7 @@ export const getPaths = (src: string) => {
assert(prev);
const backTrackCP = (
index: number,
currentPoint: { x: number; y: number },
currentPoint: { x: number; y: number }
): { x: number; y: number } => {
const previousCommand = commands[index - 1];
if (!previousCommand) {
@@ -211,7 +211,7 @@ export const getPaths = (src: string) => {
{
cp1: { x: prevCP.x, y: prevCP.y },
cp2: { x: prevCP.x, y: prevCP.y },
},
}
);
break;
}
@@ -226,13 +226,13 @@ export const getPaths = (src: string) => {
c.lArcFlag,
c.sweepFlag,
c.x,
c.y,
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}`,
{ circle: c.rX === c.rY ? { ...center, r: c.rX } : undefined },
{ circle: c.rX === c.rY ? { ...center, r: c.rX } : undefined }
);
break;
}
@@ -253,7 +253,7 @@ export const arcEllipseCenter = (
fa: number,
fs: number,
x2: number,
y2: number,
y2: number
) => {
const phi = (a * Math.PI) / 180;
@@ -280,7 +280,7 @@ export const arcEllipseCenter = (
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),
(rx * rx * y1p * y1p + ry * ry * x1p * x1p)
);
const V2 = [(rx * y1p) / ry, (-ry * x1p) / rx];

View File

@@ -1,34 +1,28 @@
import fs from 'fs';
import path from 'path';
import { Category, IconEntity } from '../theme/types';
import fs from "fs";
import path from "path";
import {Category, IconEntity} from "../theme/types";
const directory = path.join(process.cwd(), '../categories');
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 name = path.basename(fileName, '.json')
const fileContent = fs.readFileSync(path.join(directory, fileName), 'utf8')
const parsedFileContent = JSON.parse(fileContent);
const parsedFileContent = JSON.parse(fileContent)
return {
name,
title: parsedFileContent.title,
};
}
});
}
export function mapCategoryIconCount(
categories: Category[],
icons: { categories: IconEntity['categories'] }[],
) {
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,
),
}));
iconCount: icons.reduce((acc, curr) => (curr.categories.includes(category.name) ? ++acc : acc), 0)
}))
}

View File

@@ -1,161 +0,0 @@
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
type CodeExampleType = {
title: string;
language: string;
code: string;
}[];
const getIconCodes = (): CodeExampleType => {
return [
{
language: 'js',
title: 'Vanilla',
code: `\
import { createIcons, icons } from 'lucide';
import { $CamelCase } from '@lucide/lab';
createIcons({
icons: {
$CamelCase
}
});
document.body.append('<i data-lucide="$Name"></i>');\
`,
},
{
language: 'tsx',
title: 'React',
code: `import { Icon } from 'lucide-react';
import { $CamelCase } from '@lucide/lab';
const App = () => {
return (
<Icon iconNode={$CamelCase} />
);
};
export default App;
`,
},
{
language: 'vue',
title: 'Vue',
code: `<script setup>
import { Icon } from 'lucide-vue-next';
import { $CamelCase } from '@lucide/lab';
</script>
<template>
<Icon :iconNode="burger" />
</template>
`,
},
{
language: 'svelte',
title: 'Svelte',
code: `<script>
import { Icon } from 'lucide-svelte';
import { $CamelCase } from '@lucide/lab';
</script>
<Icon iconNode={burger} />
`,
},
{
language: 'tsx',
title: 'Preact',
code: `import { Icon } from 'lucide-preact';
import { $CamelCase } from '@lucide/lab';
const App = () => {
return (
<Icon iconNode={$CamelCase} />
);
};
export default App;
`,
},
{
language: 'tsx',
title: 'Solid',
code: `import { Icon } from 'lucide-solid';
import { $CamelCase } from '@lucide/lab';
const App = () => {
return (
<Icon iconNode={$CamelCase} />
);
};
export default App;
`,
},
{
language: 'tsx',
title: 'Angular',
code: `// app.module.ts
import { LucideAngularModule } from 'lucide-angular';
import { $CamelCase } from '@lucide/lab';
@NgModule({
imports: [
LucideAngularModule.pick({ $CamelCase })
],
})
// app.component.html
<lucide-icon name="$CamelCase"></lucide-icon>
`,
},
];
};
export type ThemeOptions =
| ThemeRegistration
| { light: ThemeRegistration; dark: ThemeRegistration };
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});
const highlightedCode = highlighter
.codeToHtml(code, {
lang,
themes: {
light: 'github-light',
dark: 'github-dark',
},
defaultColor: false,
})
.replace('shiki-themes', 'shiki-themes vp-code');
return `<div class="language-${lang} ${active ? 'active' : ''}">
<button title="Copy Code" class="copy"></button>
<span class="lang">${lang}</span>
${highlightedCode}
</div>`;
};
export default async function createCodeExamples() {
const codes = getIconCodes();
const codeExamplePromises = codes.map(async ({ title, language, code }, index) => {
const isFirst = index === 0;
const codeString = await highLightCode(code, language, isFirst);
return {
title,
language: language,
code: codeString,
};
});
return Promise.all(codeExamplePromises);
}

View File

@@ -1,32 +0,0 @@
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
export type ThemeOptions =
| ThemeRegistration
| { light: ThemeRegistration; dark: ThemeRegistration };
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});
const highlightedCode = highlighter
.codeToHtml(code, {
lang,
themes: {
light: 'github-light',
dark: 'github-dark',
},
defaultColor: false,
})
.replace('shiki-themes', 'shiki-themes vp-code');
return `<div class="language-${lang} ${active ? 'active' : ''}">
<button title="Copy Code" class="copy"></button>
<span class="lang">${lang}</span>
${highlightedCode}
</div>`;
};
export default highLightCode;

View File

@@ -1,5 +0,0 @@
export type CodeExampleType = {
title: string;
language: string;
code: string;
}[];

View File

@@ -1,33 +1,33 @@
import { bundledLanguages, type ThemeRegistration } from 'shikiji';
import { getHighlighter } from 'shikiji';
import {
bundledLanguages,
type ThemeRegistration
} from 'shikiji'
import {
getHighlighter,
} from 'shikiji'
type CodeExampleType = {
title: string;
language: string;
code: string;
}[];
title: string,
language: string,
code: string,
}[]
const getIconCodes = (): CodeExampleType => {
return [
{
language: 'js',
title: 'Vanilla',
code: `\
import { createIcons, icons } from 'lucide';
createIcons({ icons });
document.body.append('<i data-lucide="$Name"></i>');\
`,
language: 'html',
title: 'HTML',
code: `<i data-lucide="Name"></i>`
},
{
language: 'tsx',
title: 'React',
code: `import { $PascalCase } from 'lucide-react';
code: `import { PascalCase } from 'lucide-react';
const App = () => {
return (
<$PascalCase />
<PascalCase />
);
};
@@ -38,11 +38,11 @@ export default App;
language: 'vue',
title: 'Vue',
code: `<script setup>
import { $PascalCase } from 'lucide-vue-next';
import { PascalCase } from 'lucide-vue-next';
</script>
<template>
<$PascalCase />
<PascalCase />
</template>
`,
},
@@ -50,20 +50,20 @@ import { $PascalCase } from 'lucide-vue-next';
language: 'svelte',
title: 'Svelte',
code: `<script>
import { $PascalCase } from 'lucide-svelte';
import { PascalCase } from 'lucide-svelte';
</script>
<$PascalCase />
<PascalCase />
`,
},
{
language: 'tsx',
title: 'Preact',
code: `import { $PascalCase } from 'lucide-preact';
code: `import { PascalCase } from 'lucide-preact';
const App = () => {
return (
<$PascalCase />
<PascalCase />
);
};
@@ -73,11 +73,11 @@ export default App;
{
language: 'tsx',
title: 'Solid',
code: `import { $PascalCase } from 'lucide-solid';
code: `import { PascalCase } from 'lucide-solid';
const App = () => {
return (
<$PascalCase />
<PascalCase />
);
};
@@ -88,16 +88,16 @@ export default App;
language: 'tsx',
title: 'Angular',
code: `// app.module.ts
import { LucideAngularModule, $PascalCase } from 'lucide-angular';
import { LucideAngularModule, PascalCase } from 'lucide-angular';
@NgModule({
imports: [
LucideAngularModule.pick({ $PascalCase })
LucideAngularModule.pick({ PascalCase })
],
})
// app.component.html
<lucide-icon name="$Name"></lucide-icon>
<lucide-icon name="Name"></lucide-icon>
`,
},
{
@@ -107,39 +107,38 @@ import { LucideAngularModule, $PascalCase } from 'lucide-angular';
@import ('~lucide-static/font/Lucide.css');
</style>
<div class="icon-$Name"></div>
<div class="icon-Name"></div>
`,
},
];
};
}
]
}
export type ThemeOptions =
| ThemeRegistration
| { light: ThemeRegistration; dark: ThemeRegistration };
| { light: ThemeRegistration; dark: ThemeRegistration }
const highLightCode = async (code: string, lang: string, active?: boolean) => {
const highlighter = await getHighlighter({
themes: ['github-light', 'github-dark'],
langs: Object.keys(bundledLanguages),
});
langs: Object.keys(bundledLanguages)
})
const highlightedCode = highlighter
.codeToHtml(code, {
const highlightedCode = highlighter.codeToHtml(code, {
lang,
themes: {
light: 'github-light',
dark: 'github-dark',
dark: 'github-dark'
},
defaultColor: false,
})
.replace('shiki-themes', 'shiki-themes vp-code');
defaultColor: false
}).replace('shiki-themes', 'shiki-themes vp-code')
return `<div class="language-${lang} ${active ? 'active' : ''}">
<button title="Copy Code" class="copy"></button>
<span class="lang">${lang}</span>
${highlightedCode}
</div>`;
};
</div>`
}
export default async function createCodeExamples() {
const codes = getIconCodes();
@@ -154,7 +153,7 @@ export default async function createCodeExamples() {
language: language,
code: codeString,
};
});
})
return Promise.all(codeExamplePromises);
}

View File

@@ -1,36 +1,38 @@
import { promises as fs, constants } from 'fs';
import path from 'path';
import yaml from 'js-yaml'
import { PackageItem } from '../theme/types';
const fileExist = (filePath) =>
fs
.access(filePath, constants.F_OK)
.then(() => true)
.catch(() => false);
const fileExist = (filePath) => fs.access(filePath, constants.F_OK).then(() => true).catch(() => false)
const fetchPackages = async (): Promise<PackageItem[]> => {
const docsDir = path.resolve(process.cwd(), '../packages');
const fileNames = await (
await fs.readdir(docsDir)
).map((filename) => ({ filename, directory: docsDir }));
const fileNames = await (await fs.readdir(docsDir)).map(filename => ({filename, directory: docsDir}))
const packageJsons = await Promise.all(
fileNames.map(async ({ filename, directory }) => {
const filePath = path.resolve(directory, filename);
const packageJsons = await Promise.all(fileNames.map( async ({filename, directory}) => {
const filePath = path.resolve(directory, filename)
const fileStat = await fs.lstat(filePath);
if (!fileStat.isDirectory()) return null;
if(!fileStat.isDirectory()) return null;
const jsonFilePath = path.resolve(filePath, 'package.json');
const jsonFilePath = path.resolve(filePath, 'package.json')
if (await fileExist(jsonFilePath)) {
return JSON.parse(await fs.readFile(jsonFilePath, 'utf-8'));
return JSON.parse(
await fs.readFile(jsonFilePath, 'utf-8')
)
}
return null;
}),
const ymlFilePath = path.resolve(filePath, 'pubspec.yaml')
if(await fileExist(ymlFilePath)) {
return yaml.load(
await fs.readFile(ymlFilePath, 'utf-8')
);
}
return packageJsons;
};
return null
}))
return packageJsons
}
export default fetchPackages;

View File

@@ -1,15 +1,17 @@
export type IconContent = [icon: string, src: string];
export type IconContent = [icon: string, src:string];
async function generateZip(icons: IconContent[]) {
const JSZip = (await import('jszip')).default;
const JSZip = (await import('jszip')).default
const zip = new JSZip();
const addingZipPromises = icons.map(([name, src]) => zip.file(`${name}.svg`, src));
const addingZipPromises = icons.map(([name, src]) =>
zip.file(`${name}.svg`, src),
);
await Promise.all(addingZipPromises);
await Promise.all(addingZipPromises)
return zip.generateAsync({ type: 'blob' });
}
export default generateZip;
export default generateZip

View File

@@ -1,15 +1,17 @@
import { createLucideIcon } from 'lucide-react/src/lucide-react';
import { type LucideProps, type IconNode } from 'lucide-react/src/createLucideIcon';
import { IconEntity } from '../theme/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';
import { IconContent } from "./generateZip";
const getFallbackZip = (icons: IconEntity[], params: LucideProps) => {
return icons.map<IconContent>((icon) => {
const Icon = createLucideIcon(icon.name, icon.iconNode as IconNode);
const src = renderToStaticMarkup(<Icon {...params} />);
return [icon.name, src];
});
};
return icons
.map<IconContent>((icon) => {
const Icon = createLucideIcon(icon.name, icon.iconNode as IconNode)
const src = renderToStaticMarkup(<Icon {...params} />)
return [icon.name, src]
})
}
export default getFallbackZip;
export default getFallbackZip

View File

@@ -1,34 +1,34 @@
import fs from 'fs';
import path from 'path';
import { IconNodeWithKeys } from '../theme/types';
import iconNodes from '../data/iconNodes';
import releaseMeta from '../data/releaseMetaData.json';
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');
const directory = path.join(process.cwd(), "../icons");
export interface GetDataOptions {
withChildKeys?: boolean;
withChildKeys?: boolean
}
export async function getData(name: string) {
const jsonPath = path.join(directory, `${name}.json`);
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
const jsonContent = fs.readFileSync(jsonPath, "utf8");
const { tags, categories, contributors } = JSON.parse(jsonContent);
const iconNode = iconNodes[name];
const iconNode = iconNodes[name]
const releaseData = releaseMeta?.[name] ?? {
createdRelease: {
version: '0.0.0',
date: DATE_OF_FORK,
"createdRelease": {
"version": "0.0.0",
"date": DATE_OF_FORK
},
changedRelease: {
version: '0.0.0',
date: DATE_OF_FORK,
},
};
"changedRelease": {
"version": "0.0.0",
"date": DATE_OF_FORK
}
}
return {
name,
@@ -36,11 +36,11 @@ export async function getData(name: string) {
categories,
iconNode,
contributors,
...releaseData,
...releaseData
};
}
export async function getAllData(): Promise<{ name: string; iconNode: IconNodeWithKeys }[]> {
export async function getAllData(): Promise<{ name: string, iconNode: IconNodeWithKeys}[]> {
const names = Object.keys(iconNodes);
return Promise.all(names.map((name) => getData(name)));

View File

@@ -1,50 +1,50 @@
import { DefaultTheme, UserConfig } from 'vitepress';
import { DefaultTheme, UserConfig } from "vitepress"
const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
guide: [
'guide':[
{
text: 'Introduction',
items: [
{ text: 'What is lucide?', link: '/guide/' },
{ text: 'Installation', link: '/guide/installation' },
{ text: 'Comparison', link: '/guide/comparison' },
],
{ text: 'Comparison', link: '/guide/comparison' }
]
},
{
text: 'Basics',
items: [
{
text: 'Color',
link: '/guide/basics/color',
link: '/guide/basics/color'
},
{
text: 'Sizing',
link: '/guide/basics/sizing',
link: '/guide/basics/sizing'
},
{
text: 'Stroke width',
link: '/guide/basics/stroke-width',
link: '/guide/basics/stroke-width'
},
],
]
},
// TODO: Add this section
{
text: 'Advanced',
items: [
{
text: 'Accessibility',
link: '/guide/advanced/accessibility',
},
// {
// text: 'Accessibility',
// link: '/guide/advanced/accessibility'
// },
{
text: 'Global styling',
link: '/guide/advanced/global-styling',
link: '/guide/advanced/global-styling'
},
// {
// text: 'Animations',
// },
{
text: 'Filled icons',
link: '/guide/advanced/filled-icons',
link: '/guide/advanced/filled-icons'
},
// {
// text: 'Combining icons',
@@ -55,77 +55,75 @@ const sidebar: UserConfig<DefaultTheme.Config>['themeConfig']['sidebar'] = {
// {
// text: 'Auto importing'
// },
],
]
},
{
text: 'Packages',
items: [
{
text: 'Lucide',
link: '/guide/packages/lucide',
link: '/guide/packages/lucide'
},
{
text: 'Lucide React',
link: '/guide/packages/lucide-react',
link: '/guide/packages/lucide-react'
},
{
text: 'Lucide React Native',
link: '/guide/packages/lucide-react-native',
link: '/guide/packages/lucide-react-native'
},
{
text: 'Lucide Vue',
link: '/guide/packages/lucide-vue-next',
link: '/guide/packages/lucide-vue-next'
},
{
text: 'Lucide Svelte',
link: '/guide/packages/lucide-svelte',
link: '/guide/packages/lucide-svelte'
},
{
text: 'Lucide Solid',
link: '/guide/packages/lucide-solid',
link: '/guide/packages/lucide-solid'
},
{
text: 'Lucide Preact',
link: '/guide/packages/lucide-preact',
link: '/guide/packages/lucide-preact'
},
{
text: 'Lucide Angular',
link: '/guide/packages/lucide-angular',
link: '/guide/packages/lucide-angular'
},
{
text: 'Lucide Static',
link: '/guide/packages/lucide-static',
link: '/guide/packages/lucide-static'
},
],
]
},
{
text: 'Contributing',
items: [
{
text: 'Icon Design Principles',
link: '/guide/design/icon-design-guide',
link: '/guide/design/icon-design-guide'
},
{
text: 'Designing in Illustrator',
link: '/guide/design/illustrator-guide',
link: '/guide/design/illustrator-guide'
},
{
text: 'Designing in InkScape',
link: '/guide/design/inkscape-guide',
link: '/guide/design/inkscape-guide'
},
{
text: 'Designing in Figma',
link: '/guide/design/figma-guide',
link: '/guide/design/figma-guide'
},
{
text: 'Designing in Affinity Designer',
link: '/guide/design/affinity-designer-guide',
},
],
]
},
],
// This should be here to keep the sidebar shown on the icons page
icons: [{ text: '', link: '/' }],
};
'icons': [
{ text: '', link: '/' },
],
}
export default sidebar;
export default sidebar

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vitepress';
const { go } = useRouter()
@@ -7,16 +8,7 @@ const props = defineProps<{
href?: string
}>()
const isExternal = computed(() => props.href?.startsWith('http') ?? false)
const component = computed(() => props.href ? 'a' : 'div')
const target = computed(() => isExternal.value ? '_blank' : undefined)
const rel = computed(() => isExternal.value ? 'noreferrer noopener' : undefined)
const onClick = computed(() => {
if(!props.href || isExternal) return
return go(props.href)
})
</script>
<template>
@@ -24,9 +16,7 @@ const onClick = computed(() => {
:is="component"
:href="href"
class="badge"
:target="target"
:rel="rel"
@click="onClick"
@click="props?.href ? go(href) : undefined"
>
<slot/>
</component>

View File

@@ -1,11 +1,11 @@
<template>
<div class="card-grid-flex">
<div class="grid">
<slot />
</div>
</template>
<style>
.card-grid-flex {
<style scoped>
.grid {
display: flex;
flex-wrap: wrap;
align-items: stretch;
@@ -15,20 +15,20 @@
margin: -8px;
}
.card-grid-flex > * {
.grid > * {
flex-basis: 100%;
box-sizing: border-box;
padding: 8px;
}
@media (min-width: 960px) {
.card-grid-flex > * {
.grid > * {
flex-basis: 50%;
}
}
@media (min-width: 1280px) {
.card-grid-flex > * {
.grid > * {
flex-basis: 33.33%;
}
}

View File

@@ -1,90 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
label: string
id: string
value: string
modelValue: string | string[]
}>()
const emit = defineEmits(['change', 'input', 'update:modelValue'])
const model = computed({
get: () => {
if (Array.isArray(props.modelValue)) {
return props.modelValue.includes(props.value)
}
return props.modelValue === props.value
},
set: (value: string) => {
if (Array.isArray(props.modelValue)) {
const newValue = [...props.modelValue]
const index = newValue.indexOf(props.value)
if (value) {
if (index === -1) {
newValue.push(props.value)
}
} else {
if (index !== -1) {
newValue.splice(index, 1)
}
}
emit('update:modelValue', newValue)
} else {
emit('update:modelValue', value)
}
}
})
</script>
<template>
<div class="checkbox-wrapper">
<input
type="checkbox"
class="checkbox"
ref="input"
:id="id"
v-model="model"
v-bind="$attrs"
/>
<label :for="id" class="checkbox-label">
{{ label }}
</label>
</div>
</template>
<style scoped>
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-label {
line-height: 20px;
font-size: 13px;
color: var(--vt-c-text-1);
transition: color .5s;
display: block;
}
.checkbox {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
cursor: pointer;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
border-radius: 4px;
}
.checkbox:checked {
border-color: transparent;
background: url("data:image/svg+xml,%3Csvg width='12px' height='12px' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='4' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E")
center no-repeat var(--vp-c-brand);;
}
</style>

View File

@@ -24,10 +24,40 @@ const headingElement = computed(() => `h${props.headingLevel}`)
font-size: 32px;
font-weight: bold;
text-align: center;
margin-bottom: 36px;
margin-bottom: 32px;
}
section {
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>

View File

@@ -1,18 +1,16 @@
export default {
async load() {
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest')
.then((res) => {
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 }>;
const releaseData = res.json() as Promise<{ tag_name: string }>
return releaseData;
return releaseData
}
return null;
})
.then((res) => res.tag_name);
return null
}).then(res => res.tag_name)
return {
version,
};
},
};
version
}
}
}

View File

@@ -8,6 +8,8 @@ import { data } from './HomeHeroBefore.data'
<HomeContainer class="container">
<Badge
:href="`https://github.com/lucide-icons/lucide/releases/tag/${data.version}`"
target="_blank"
rel="noreferrer noopener"
>v{{ data.version }}</Badge>
</HomeContainer>
</template>

View File

@@ -1,17 +1,16 @@
import iconNodes from '../../../data/iconNodes';
import iconNodes from '../../../data/iconNodes'
const getRandomItem = <Item>(items: Item[]): Item =>
items[Math.floor(Math.random() * items.length)];
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 icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }))
const randomIcons = Array.from({ length: 200 }, () => getRandomItem(icons));
const randomIcons = Array.from({ length: 200 }, () => getRandomItem(icons))
return {
icons: randomIcons,
iconsCount: icons.length,
};
},
};
}
}
}

View File

@@ -28,6 +28,8 @@ function insert() {
const replaceIndex = random(0, 48)
const newIcon = getRandomNewIcon()
// items.value.splice(replaceIndex, 0, newIcon);
items.value[replaceIndex] = newIcon
}
@@ -74,6 +76,7 @@ onBeforeUnmount(() => {
<style scoped>
.card-wrapper {
/* padding: 0 24px; */
margin-left: auto;
margin-bottom: auto;
margin-top: 48px;
@@ -84,10 +87,13 @@ onBeforeUnmount(() => {
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 {
@@ -101,6 +107,7 @@ onBeforeUnmount(() => {
max-width: 512px;
overflow: hidden;
position: relative;
/* white-space: nowrap; */
}
.list-enter-active {

View File

@@ -47,7 +47,7 @@ export default {
logo: '/framework-logos/flutter.svg',
label: 'Lucide documentation for Flutter',
},
],
};
},
};
]
}
}
}

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import HomeContainer from './HomeContainer.vue'
import HomeSectionTitle from './HomeSectionTitle.vue'
import { useRouter } from 'vitepress';
import { data } from './HomePackagesSection.data'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
@@ -10,7 +9,7 @@ const { go } = useRouter()
<template>
<HomeContainer>
<HomeSectionTitle>Available For:</HomeSectionTitle>
<h2 class="section-title">Available For:</h2>
<div class="packages-list">
<a
v-for="{ name, logo } in data.packages"
@@ -35,6 +34,14 @@ const { go } = useRouter()
</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;

View File

@@ -1,26 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
headingLevel: 1 | 2 | 3 | 4 | 5 | 6,
}>()
const headingElement = computed(() => `h${props.headingLevel ?? 2}`)
</script>
<template>
<component :is="headingElement" class="section-title">
<slot />
</component>
</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;
}
</style>

View File

@@ -1,59 +0,0 @@
<script setup lang="ts">
import Card from '../base/Card.vue';
import HomeSectionTitle from './HomeSectionTitle.vue';
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue';
</script>
<template>
<HomeSectionTitle :headingLevel="3"> Sponsor the Lucide maintainers </HomeSectionTitle>
<Card class="sponsor-card">
<img
src="/company-logos/open-collective-light.svg"
alt="Open Collective logo"
class="logo light"
width="242"
height="42"
/>
<img
src="/company-logos/open-collective-dark.svg"
alt="Open Collective logo"
class="logo dark"
width="242"
height="42"
/>
<VPButton
href="https://opencollective.com/lucide-icons"
class="sponsor-button"
text="Become a sponsor"
/>
</Card>
</template>
<style scoped>
.sponsor-card {
margin: 0 auto;
max-width: 500px;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.sponsor-button {
margin: auto 0;
}
html.dark .logo.dark {
display: none;
}
html:not(.dark) .logo.light {
display: none;
}
@media (min-width: 640px) {
.sponsor-card {
flex-direction: row;
justify-content: space-between;
}
}
</style>

View File

@@ -1,91 +0,0 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import HomeContainer from './HomeContainer.vue'
import GridSection from '../base/GridSection.vue'
import TeamMemberCard, { TeamMember } from './TeamMemberCard.vue'
import teamData from '../../../data/teamData.json'
import HomeSponsorCard from './HomeSponsorCard.vue';
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
const { theme } = useData()
</script>
<template>
<HomeContainer>
<GridSection
title="Meet the team"
:headingLevel="2"
class="team-members"
>
<TeamMemberCard
v-for="teamMember in (teamData as TeamMember[])"
v-bind="teamMember"
/>
</GridSection>
<HomeSponsorCard />
<VPDocAsideCarbonAds
:carbon-ads="theme.carbonAds"
class="ad-card"
/>
</HomeContainer>
</template>
<style scoped>
.team-members {
gap: 24px;
margin-top: 48px;
margin-bottom: 48px;
}
@media (min-width: 640px) {
.team-members :deep(.card-grid > *) {
flex-basis: 50%;
}
}
@media (min-width: 768px) {
.team-members :deep(.card-grid > *) {
flex-basis: 33.33%;
}
}
.ad-card {
margin: 32px auto 0;
width: 100%;;
max-width: 500px;
display: flex;
flex-direction: row;
justify-content: space-between;
display: none;
}
@media (min-width: 960px) {
.ad-card {
display: block;
}
}
.ad-card :deep(.VPCarbonAds) {
width: 100%;
text-align: left;
min-height: auto;
}
.ad-card :deep(.VPCarbonAds .carbon-wrap) {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 24px;
}
.ad-card :deep(.VPCarbonAds .carbon-text) {
padding-top: 0;
}
.ad-card :deep(.VPCarbonAds .carbon-poweredby) {
text-align: right;
margin-top: -24px;
}
</style>

View File

@@ -1,91 +0,0 @@
<script lang="ts">
export interface TeamMember {
name: string
title: string
image: string
sponsor?: string
socialLinks: DefaultTheme.SocialLink[]
}
</script>
<script setup lang="ts">
import { DefaultTheme } from 'vitepress';
import Card from '../base/Card.vue'
import VPButton from 'vitepress/dist/client/theme-default/components/VPButton.vue'
import VPSocialLinks from 'vitepress/dist/client/theme-default/components/VPSocialLinks.vue'
defineProps<TeamMember>()
</script>
<template>
<div>
<Card class="member-card">
<img :src="image" :alt="name" class="member-image"/>
<h3 class="member-name">
{{name}}
</h3>
<p class="member-title">
{{title}}
</p>
<div class="member-links">
<VPButton
v-if="sponsor"
:href="sponsor"
text="Sponsor"
theme="sponsor"
class="sponsor-button"
size="medium"
/>
<VPSocialLinks
:links="socialLinks"
/>
</div>
</Card>
</div>
</template>
<style scoped>
.member-card {
flex-basis: 100%;
height:100%;
}
.member-image {
width: 64px;
height: 64px;
border-radius: 32px;
margin: 0 auto;
background-color: var(--vp-c-bg);
}
.member-name {
text-align: center;
margin-top: 16px;
font-size: 21px;
font-weight: 500;
color: var(--textColor);
}
.member-title {
flex-grow: 1;
padding-top: 8px;
line-height: 24px;
font-size: 14px;
font-weight: 500;
color: var(--vp-c-text-2);
text-align: center;
margin-bottom: 16px;;
}
.sponsor-button {
width: auto;
}
.member-links {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
</style>

View File

@@ -1,83 +0,0 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import { useSessionStorage } from '@vueuse/core';
import IconButton from '../base/IconButton.vue';
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue'
import { x } from '../../../data/iconNodes'
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
const { theme } = useData()
const showAd = useSessionStorage('show-carbon-ads',true)
defineProps<{
drawerOpen: boolean
}>()
const CloseIcon = createLucideIcon('Close', x)
</script>
<template>
<div
:class="{
'drawer-open': drawerOpen,
'hide-ad': !showAd
}"
class="floating-ad"
v-if="theme.carbonAds"
>
<IconButton @click="showAd = false" class="hide-button">
<component :is="CloseIcon" :size="20" absoluteStrokeWidth />
</IconButton>
<VPDocAsideCarbonAds
:carbon-ads="theme.carbonAds"
/>
</div>
</template>
<style scoped>
.floating-ad {
display: none;
position: fixed;
bottom: 32px;
width: 224px;
right: 32px;
transition: opacity 0.5s, transform 0.25s ease;
}
.floating-ad.drawer-open {
transform: translateY(-288px);
}
.floating-ad.hide-ad {
transform: translateX(224px);
opacity: 0;
}
.floating-ad.drawer-open.hide-ad {
transform: translateY(-288px) translateX(224px);
}
.floating-ad.drawer-open, .floating-ad.hide-ad {
transition: opacity 0.25s, transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
@media (min-width: 1280px) {
.floating-ad {
display: block;
}
}
@media (min-width: 1440px) {
.floating-ad {
right: calc(((100% - (var(--vp-layout-max-width) - var(--vp-sidebar-width))) - 272px) / 2);
}
}
.hide-button {
padding: 4px;
position: absolute;
top: 8px;
right: 8px;
background-color: transparent;
}
</style>

View File

@@ -1,15 +1,16 @@
import { getAllData } from '../../../lib/icons';
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
import iconsMetaData from '../../../data/iconMetaData';
import iconsMetaData from '../../../data/iconMetaData'
export default {
async load() {
let categories = getAllCategoryFiles();
let categories = getAllCategoryFiles()
categories = mapCategoryIconCount(categories, Object.values(iconsMetaData));
categories = mapCategoryIconCount(categories, Object.values(iconsMetaData))
return {
categories,
};
},
};
}
}
}

View File

@@ -1,16 +1,13 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
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'
import SidebarTitle from './SidebarTitle.vue'
import { useCategoryView } from '../../composables/useCategoryView'
const { page } = useData()
const { categoryCounts } = useCategoryView();
const categoriesIsActive = computed(() => {
return isActive(page.value.relativePath, '/icons/categories');
@@ -28,32 +25,22 @@ const headers = computed(() => {
level: 2,
link: `${linkPrefix}#${name}`,
title,
iconCount: categoryCounts.value[name] ?? iconCount,
name
iconCount
}))
})
const container = ref()
const marker = ref()
const { setActiveLinkDebounced } = useActiveAnchor(container, marker)
watch(headers, () => {
setTimeout(() => {
setActiveLinkDebounced()
}, 200)
})
useActiveAnchor(container, marker)
</script>
<template>
<div class="category-list" ref="container">
<SidebarTitle>
View
</SidebarTitle>
<VPLink class="sidebar-link sidebar-text" href="/icons/" :class="{ 'active': overviewIsActive } ">
<VPLink class="sidebar-title" href="/icons/" :class="{ 'active': overviewIsActive } ">
All
</VPLink>
<VPLink class="sidebar-link sidebar-text" href="/icons/categories" :class="{ 'active': categoriesIsActive } ">
<VPLink class="sidebar-title" href="/icons/categories" :class="{ 'active': categoriesIsActive } ">
Categories
</VPLink>
<div class="content">
@@ -66,20 +53,17 @@ watch(headers, () => {
</template>
<style scoped>
.sidebar-text {
.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;
padding: 4px 0;
}
.sidebar-link {
font-weight: 500;
color: var(--vp-c-text-2);
}
.sidebar-link:hover, .sidebar-link.active {
.sidebar-title:hover, .sidebar-title.active {
color: var(--vp-c-brand);
}
.content {

View File

@@ -7,7 +7,6 @@ interface Header {
slug: string;
iconCount: number;
link: string;
name: string;
children: Header[];
}
@@ -15,35 +14,36 @@ type MenuItem = Omit<Header, 'slug' | 'children'> & {
children?: MenuItem[];
};
defineProps<{
const props = defineProps<{
headers: MenuItem[];
root?: boolean;
}>();
const { selectedCategory } = useCategoryView();
function onClick(categoryName: string) {
selectedCategory.value = categoryName;
function onClick(event: Event) {
const target =
(event.target as HTMLElement).nodeName === 'span'
? (event.target as HTMLElement).parentNode
: (event.target as HTMLElement);
const href = (target as HTMLAnchorElement)?.href;
const heading = document.querySelector<HTMLAnchorElement>(categoryName);
if (href) {
const id = '#' + href.split('#')[1];
const decodedId = decodeURIComponent(id);
selectedCategory.value = decodedId.replace('#', '');
const heading = document.querySelector<HTMLAnchorElement>(decodedId);
heading?.focus();
window.history.pushState({}, '', `/icons/categories#${categoryName}`)
}
}
</script>
<template>
<ul :class="root ? 'root' : 'nested'">
<li v-for="{ children, link, title, iconCount, name } in headers">
<a
class="outline-link"
:href="link"
@click="onClick(name)"
:title="title"
:class="{
inactive: iconCount === 0,
}"
>
<li v-for="{ children, link, title, iconCount } in headers">
<a class="outline-link" :href="link" @click="onClick" :title="title">
<span>
{{ title }}
</span>
@@ -83,10 +83,6 @@ function onClick(categoryName: string) {
transition: color 0.25s;
}
.outline-link.inactive {
color: var(--vp-c-text-4);
}
.outline-link.nested {
padding-left: 13px;
}
@@ -97,8 +93,4 @@ function onClick(categoryName: string) {
font-size: 11px;
font-weight: 400;
}
.outline-link.inactive .icon-count {
opacity: 0;
}
</style>

View File

@@ -39,12 +39,6 @@ function copyJSX() {
navigator.clipboard.writeText(code)
}
function copyComponentName() {
const code = componentName.value
navigator.clipboard.writeText(code)
}
function copyVue() {
let attrs = ['']
@@ -107,7 +101,6 @@ function copyAngular() {
:popoverPosition="popoverPosition"
:options="[
{ text: 'Copy JSX' , onClick: copyJSX },
{ text: 'Copy Component Name' , onClick: copyComponentName },
{ text: 'Copy Vue' , onClick: copyVue },
{ text: 'Copy Svelte' , onClick: copyJSX },
{ text: 'Copy Angular' , onClick: copyAngular },

View File

@@ -11,32 +11,15 @@ import IconInfo from './IconInfo.vue';
import Badge from '../base/Badge.vue';
import { computedAsync } from '@vueuse/core';
import { satisfies } from 'semver';
import { useExternalLibs } from '../../composables/useExternalLibs';
const props = defineProps<{
iconName: string | null
iconName: string
}>()
const { externalIconNodes } = useExternalLibs()
const { go } = useRouter()
const icon = computedAsync<IconEntity | null>(async () => {
if (props.iconName) {
try {
if (props.iconName.includes(':')) {
const [library, name] = props.iconName.split(':')
return externalIconNodes.value[library].find((icon) => icon.name === name)
} else {
return (await import(`../../../data/iconDetails/${props.iconName}.ts`)).default as IconEntity
}
} catch (err) {
if (!props.iconName.includes(':')) {
go(`/icons/${props.iconName}`)
}
}
}
return null
}, null)
@@ -53,6 +36,8 @@ function onClose() {
emit('close')
}
const { go } = useRouter()
const CloseIcon = createLucideIcon('Close', x)
const Expand = createLucideIcon('Expand', expand)
</script>
@@ -66,8 +51,10 @@ const Expand = createLucideIcon('Expand', expand)
v-if="icon.createdRelease"
class="version"
:href="releaseTagLink(icon.createdRelease.version)"
target="_blank"
rel="noreferrer noopener"
>v{{ icon.createdRelease.version }}</Badge>
<IconButton @click="go(icon.externalLibrary ? `/icons/${icon.externalLibrary}/${icon.name}` : `/icons/${icon.name}`)">
<IconButton @click="go(`/icons/${icon.name}`)">
<component :is="Expand" />
</IconButton>
<IconButton @click="onClose">
@@ -157,11 +144,11 @@ const Expand = createLucideIcon('Expand', expand)
}
.drawer-enter-active {
transition: opacity 0.5s, transform 0.25s ease;
transition: all 0.2s cubic-bezier(.21,.8,.46,.9);
}
.drawer-leave-active {
transition: opacity 0.25s ease, transform 1.6s ease-out;
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
}
.drawer-enter-from,

View File

@@ -4,7 +4,7 @@ import IconItem from './IconItem.vue'
const emit = defineEmits(['setActiveIcon'])
defineProps<{
const props = defineProps<{
icons: IconEntity[]
activeIcon?: string
overlayMode?: boolean
@@ -25,10 +25,8 @@ function setActiveIcon(name: string) {
:key="icon.name"
>
<IconItem
:iconNode="icon.iconNode"
:name="icon.name"
:externalLibrary="icon.externalLibrary"
@setActiveIcon="setActiveIcon"
v-bind="icon"
@setActiveIcon="setActiveIcon(icon.name)"
:active="activeIcon === icon.name"
customizable
:overlayMode="overlayMode"
@@ -42,6 +40,7 @@ function setActiveIcon(name: string) {
.icons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
/* padding: 32px 32px 96px; */
gap: 8px;
width: 100%;
}

View File

@@ -7,8 +7,6 @@ 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';
import createLucideIcon from 'lucide-vue-next/src/createLucideIcon';
import { diamond } from '../../../data/iconNodes'
const props = defineProps<{
icon: IconEntity
@@ -22,21 +20,13 @@ const tags = computed(() => {
if (!props.icon || !props?.icon?.tags) return []
return props.icon.tags.join(' • ')
})
const DiamondIcon = createLucideIcon('Diamond', diamond)
</script>
<template>
<div class="icon-info">
<div class="icon-name-wrapper">
<IconDetailName class="icon-name">
{{ icon.name }}
</IconDetailName>
<div v-if="icon.externalLibrary" class="icon-external-lib">
<DiamondIcon fill="currentColor" :size="12"/>
{{ icon.externalLibrary }}
</div>
</div>
<div class="tags-scroller" v-if="tags.length">
<p class="icon-tags horizontal-scroller">
{{ tags }}
@@ -54,10 +44,10 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
<div class="group buttons">
<VPButton
v-if="!page?.relativePath?.startsWith?.(icon.externalLibrary ? `icons/${icon.externalLibrary}/${icon.name}`: `icons/${icon.name}`)"
:href="icon.externalLibrary ? `/icons/${icon.externalLibrary}/${icon.name}`: `/icons/${icon.name}`"
v-if="!page?.relativePath?.startsWith?.(`icons/${icon.name}`)"
:href="`/icons/${icon.name}`"
text="See in action"
@click="go(icon.externalLibrary ? `/icons/${icon.externalLibrary}/${icon.name}`: `/icons/${icon.name}`)"
@click="go(`/icons/${icon.name}`)"
/>
<CopySVGButton :name="icon.name" :popoverPosition="popoverPosition"/>
<CopyCodeButton :name="icon.name" :popoverPosition="popoverPosition"/>
@@ -77,27 +67,9 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
text-transform: capitalize;
}
.icon-name {
margin-right: -36px;
}
.icon-name-wrapper {
display: flex;
align-items: center;
gap: 2px;
margin-bottom: 4px;
}
.icon-external-lib {
color: var(--vp-c-brand-dark);
padding: 4px 12px;
font-size: 16px;
font-weight: 600;
line-height: 28px;
display: flex;
gap: 8px;
align-items: center;
}
.icon-tags {
font-size: 16px;
color: var(--vp-c-text-2);

View File

@@ -6,7 +6,6 @@ import { useRouter } from 'vitepress';
import getSVGIcon from '../../utils/getSVGIcon';
import useConfetti from '../../composables/useConfetti';
import Tooltip from '../base/Tooltip.vue';
import { diamond } from '../../../data/iconNodes'
const downloadText = 'Download!'
const copiedText = 'Copied!'
@@ -17,7 +16,6 @@ const props = defineProps<{
name: string;
iconNode: IconNode;
active: boolean;
externalLibrary?: string;
customizable?: boolean;
overlayMode?: boolean
hideIcon?: boolean
@@ -35,9 +33,8 @@ const icon = computed(() => {
return createLucideIcon(props.name, props.iconNode)
})
const href = computed(() => props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`)
async function navigateToIcon(event) {
if (event.shiftKey) {
event.preventDefault()
const svgString = getSVGIcon(event.target.firstChild, {
@@ -53,16 +50,14 @@ async function navigateToIcon(event) {
if(props.overlayMode && showOverlay.value) {
event.preventDefault()
window.history.pushState({}, '', props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`)
emit('setActiveIcon', props.externalLibrary ? `${props.externalLibrary}:${props.name}`: props.name)
} else {
window.history.pushState({}, '', `/icons/${props.name}`)
emit('setActiveIcon', props.name)
}
else {
event.preventDefault()
go(props.externalLibrary ? `/icons/${props.externalLibrary}/${props.name}` : `/icons/${props.name}`)
go(`/icons/${props.name}`)
}
}
const DiamondIcon = createLucideIcon('Diamond', diamond)
</script>
<template>
@@ -72,7 +67,7 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
@click="navigateToIcon"
:class="{ active, animate }"
:aria-label="name"
:href="`/icons/${props.name}`"
:data-confetti-text="confettiText"
ref="ref"
>
@@ -86,13 +81,6 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
}"
/>
</KeepAlive>
<div
v-if="externalLibrary"
class="floating-diamond"
aria-hidden="true"
>
<DiamondIcon fill="currentColor" :size="8"/>
</div>
</a>
</Tooltip>
</template>
@@ -101,7 +89,6 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
<style scoped>
.icon-button {
position: relative;
display: inline-block;
border: 1px solid transparent;
text-align: center;
@@ -118,13 +105,6 @@ const DiamondIcon = createLucideIcon('Diamond', diamond)
color: var(--vp-c-text-1);
}
.floating-diamond {
position: absolute;
top: 4px;
right: 4px;
color: var(--vp-c-brand);
}
.confetti-button:before,
.confetti-button:after {
z-index: 100;

View File

@@ -1,53 +1,56 @@
<script lang="ts">
import { IconEntity } from '../../types';
type CategoryNameRow = {
type: 'category';
title: string;
name: string;
};
type CategoryIconsRow = {
type: 'icons';
icons: IconEntity[];
};
export type CategoryRow = CategoryNameRow | CategoryIconsRow;
</script>
<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 | null
categoryRow: CategoryRow
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>
<h2
v-if="categoryRow.type === 'category'"
class="title"
<section
class="category"
:key="category.name"
:id="category.name"
v-intersection-observer="onIntersectionObserver"
>
<a class="header-anchor" :href="`#${categoryRow.name}`" :aria-label="`Permalink to &quot;${categoryRow.title}&quot;`">&ZeroWidthSpace;</a>
{{ categoryRow.title }}
<h2 class="title" >
<a class="header-anchor" :href="`#${category.name}`" :aria-label="`Permalink to &quot;${category.title}&quot;`">&ZeroWidthSpace;</a>
{{ category.title }}
</h2>
<IconGrid
v-else-if="categoryRow.type === 'icons'"
:activeIcon="activeIconName"
:icons="categoryRow.icons"
:icons="category.icons"
@setActiveIcon="$event => $emit('setActiveIcon', $event)"
overlayMode
:hideIcons="!showIcons"
/>
</section>
</template>
<style scoped>
.title {
margin-bottom: 8px;
margin-bottom: 24px;
font-size: 19px;
font-weight: 500;
padding: 24px 0 8px;
padding-top: 86px;
/* scroll-padding-top: 240px; */
}
.category {
margin-bottom: calc(-86px + 32px);
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted, watch, watchEffect } from 'vue';
import { ref, computed, defineAsyncComponent } from 'vue';
import type { IconEntity, Category } from '../../types';
import useSearch from '../../composables/useSearch';
import InputSearch from '../base/InputSearch.vue';
@@ -8,14 +8,6 @@ import StickyBar from './StickyBar.vue';
import IconsCategory from './IconsCategory.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import chunkArray from '../../utils/chunkArray';
import { CategoryRow } from './IconsCategory.vue';
import useScrollToCategory from '../../composables/useScrollToCategory';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const props = defineProps<{
icons: IconEntity[];
@@ -25,6 +17,7 @@ const props = defineProps<{
const activeIconName = ref(null);
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const isSearching = computed(() => !!searchQuery.value);
function setActiveIconName(name: string) {
@@ -34,13 +27,6 @@ function setActiveIconName(name: string) {
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => {
return Math.floor((containerWidth.value) / ((ICON_SIZE + ICON_GRID_GAP)));
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
@@ -57,10 +43,9 @@ const mappedIcons = computed(() => {
});
});
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
const searchResults = useSearch(searchQuery, mappedIcons, [
{ name: 'name', weight: 2 },
{ name: 'tags', weight: 1 },
]);
const categories = computed(() => {
@@ -69,7 +54,7 @@ const categories = computed(() => {
return props.categories
.map(({ name, title }) => {
const categoryIcons = props.icons.filter((icon) => {
const iconCategories = icon?.externalLibrary ? icon.categories : props.iconCategories[icon.name]
const iconCategories = props.iconCategories[icon.name];
return iconCategories?.includes(name);
});
@@ -86,43 +71,9 @@ const categories = computed(() => {
icons: searchedCategoryIcons,
};
})
.filter(({ icons }) => icons.length);
});
const categoriesList = computed(() => {
return categories.value
.filter(({ icons }) => icons.length)
.reduce<CategoryRow[]>((acc, category) => {
acc.push({ type: 'category', title: category.title, name: category.name });
const categoryIcons = chunkArray(category.icons, columnSize.value);
categoryIcons.forEach((icons) => {
acc.push({ type: 'icons', icons });
});
return acc;
}, []);
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
categoriesList,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10
},
)
useScrollToCategory({
categories,
categoriesList,
scrollTo,
searchQueryDebounced,
})
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll)
})
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
@@ -133,24 +84,12 @@ function onFocusSearchInput() {
}
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
function handleCloseDrawer() {
setActiveIconName('');
window.history.pushState({}, '', '/icons/categories');
}
watchEffect(() => {
console.log(props.icons.find((icon) => icon.name === 'burger'));
});
</script>
<template>
<div ref="overviewEl" class="overview-container">
<StickyBar class="category-search">
<StickyBar class="search-bar category-search">
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
v-model="searchQuery"
@@ -159,28 +98,19 @@ watchEffect(() => {
@focus="onFocusSearchInput"
/>
</StickyBar>
<NoResults
v-if="categories.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<div v-bind="wrapperProps">
<NoResults v-if="categories.length === 0" :searchQuery="searchQuery" @clear="searchQuery = ''" />
<IconsCategory
v-for="{ index, data } in list"
:categoryRow="data"
v-for="category in categories"
:key="category.name"
:category="category"
:activeIconName="activeIconName"
@setActiveIcon="setActiveIconName"
:key="index"
/>
</div>
</div>
<IconDetailOverlay
v-if="activeIconName != null"
:iconName="activeIconName"
@close="handleCloseDrawer"
@close="setActiveIconName('')"
/>
<CarbonAdOverlay :drawerOpen="!!activeIconName" />
</template>
<style scoped>
@@ -189,21 +119,6 @@ watchEffect(() => {
}
.search-bar.category-search {
margin-bottom: 10px;
}
.title {
margin-bottom: 8px;
font-size: 19px;
font-weight: 500;
padding: 24px 0 8px;
}
.icons {
margin-bottom: 8px;
}
.overview-container {
padding-bottom: 288px;
margin-bottom: -54px;
}
</style>

View File

@@ -1,19 +1,15 @@
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted, watch } from 'vue';
import { ref, computed, watch, defineAsyncComponent } from 'vue';
import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import { useMediaQuery, useOffsetPagination } from '@vueuse/core';
import IconGrid from './IconGrid.vue';
import InputSearch from '../base/InputSearch.vue';
import useSearch from '../../composables/useSearch';
import EndOfPage from '../base/EndOfPage.vue';
import useSearchInput from '../../composables/useSearchInput';
import StickyBar from './StickyBar.vue';
import useFetchTags from '../../composables/useFetchTags';
import useFetchCategories from '../../composables/useFetchCategories';
import chunkArray from '../../utils/chunkArray';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const props = defineProps<{
icons: IconEntity[];
@@ -21,16 +17,32 @@ const props = defineProps<{
const activeIconName = ref(null);
const isExtraLargeScreen = useMediaQuery('(min-width: 1440px)');
const isLargeScreen = useMediaQuery('(min-width: 1280px)');
const isMediumScreen = useMediaQuery('(min-width: 960px)');
const isSmallScreen = useMediaQuery('(min-width: 640px)');
const pageSize = computed(() => {
if (isExtraLargeScreen.value) {
return 16 * 20;
}
if (isLargeScreen.value) {
return 16 * 12;
}
if (isMediumScreen.value) {
return 13 * 12;
}
if (isSmallScreen.value) {
return 10 * 10;
}
return 10 * 5;
});
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl)
const columnSize = computed(() => {
return Math.floor((containerWidth.value) / ((ICON_SIZE + ICON_GRID_GAP)));
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
@@ -51,33 +63,26 @@ const mappedIcons = computed(() => {
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
]);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
const { next, currentPage } = useOffsetPagination({ pageSize });
const paginatedIcons = computed(() => {
const end = pageSize.value * currentPage.value;
return searchResults.value.slice(0, end);
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(
chunkedIcons,
{
itemHeight: ICON_SIZE + ICON_GRID_GAP,
overscan: 10
},
)
onMounted(() => {
containerProps.ref.value = document.documentElement;
useEventListener(window, 'scroll', containerProps.onScroll)
})
function setActiveIconName(name: string) {
activeIconName.value = name;
}
watch(searchQueryDebounced, (searchString) => {
currentPage.value = 1;
});
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
@@ -90,20 +95,9 @@ function onFocusSearchInput() {
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
watch(searchQueryDebounced, () => {
scrollTo(0)
})
function handleCloseDrawer() {
setActiveIconName('');
window.history.pushState({}, '', '/icons/');
}
</script>
<template>
<div ref="overviewEl" class="overview-container">
<StickyBar>
<InputSearch
:placeholder="`Search ${icons.length} icons ...`"
@@ -114,33 +108,30 @@ function handleCloseDrawer() {
/>
</StickyBar>
<NoResults
v-if="list.length === 0"
v-if="paginatedIcons.length === 0"
:searchQuery="searchQuery"
@clear="searchQuery = ''"
/>
<div v-bind="wrapperProps" class="icon">
<IconGrid
v-for="{ index, data: icons } in list"
:key="index"
overlayMode
:icons="icons"
:activeIcon="activeIconName"
:icons="paginatedIcons"
@setActiveIcon="setActiveIconName"
/>
</div>
</div>
<EndOfPage @end-of-page="next" class="bottom-page" />
<IconDetailOverlay
v-if="activeIconName != null"
:iconName="activeIconName"
@close="handleCloseDrawer"
@close="setActiveIconName('')"
/>
<CarbonAdOverlay :drawerOpen="!!activeIconName" />
</template>
<style>
.icons {
margin-bottom: 8px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
gap: 8px;
width: 100%;
}
.icon {
@@ -151,7 +142,7 @@ function handleCloseDrawer() {
width: 100%;
}
.overview-container {
padding-bottom: 288px;
.bottom-page {
height: 288px;
}
</style>

View File

@@ -1,10 +1,9 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { bird, squirrel, rabbit } from '../../../data/iconNodes'
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'
import { IconNode } from '../../types'
defineProps<{
searchQuery: string
@@ -12,45 +11,29 @@ defineProps<{
defineEmits(['clear'])
const animalIcon = ref<HTMLElement>()
const randomAnimal = computed<IconNode>(() => {
return Math.random() > 0.5 ? squirrel : Math.random() > 0.5 ? rabbit : bird
})
const animalComponent = computed(() => createLucideIcon('animal', randomAnimal.value))
const birdIcon = ref<HTMLElement>()
const Bird = createLucideIcon('bird', bird)
const flip = ref(false)
onMounted(() => {
useEventListener(document, 'mousemove', (mouseEvent) => {
const {width, height, x, y} = animalIcon.value.getBoundingClientRect()
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">
<component
:is="animalComponent"
class="animal-icon"
ref="animalIcon"
:class="{ flip }"
:strokeWidth="1"
/>
<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')"
/>
<VPButton text="Clear your search and try again" theme="alt" @click="$emit('clear')"/>
<span class="text-divider">or</span>
<VPButton
text="Search on Github issues"
<VPButton text="Search on Github issues"
theme="alt"
:href="`https://github.com/lucide-icons/lucide/issues?q=is%3Aopen+${searchQuery}`"
target="_blank"
@@ -65,7 +48,7 @@ onMounted(() => {
align-items: center;
}
.animal-icon {
.bird-icon {
width: 160px;
height: 160px;
color: var(--vp-c-neutral);
@@ -73,12 +56,12 @@ onMounted(() => {
margin-top: 72px;
}
.animal-icon.flip {
.bird-icon.flip {
transform: rotateY(180deg);
}
@media (min-width: 960px) {
.animal-icon {
.bird-icon {
width: 240px;
height: 240px;
}

View File

@@ -1,47 +0,0 @@
<script setup lang="ts">
import Checkbox from '../base/Checkbox.vue'
import SidebarTitle from './SidebarTitle.vue'
import { useExternalLibs } from '../../composables/useExternalLibs';
import { ExternalLibs } from '../../types';
interface ExternalLibrary {
name: string;
value: ExternalLibs;
}
const externalLibraries: ExternalLibrary[] = [
{
name: 'Lab',
value: 'lab'
},
];
const { selectedLibs } = useExternalLibs();
</script>
<template>
<div class="external-library-select">
<SidebarTitle>
Include external libs
</SidebarTitle>
<ul>
<li
v-for="library in externalLibraries"
:key="library.name"
>
<Checkbox
:label="library.name"
:id="library.name"
v-model="selectedLibs"
:value="library.value"
/>
</li>
</ul>
</div>
</template>
<style scoped>
.external-library-select {
margin-bottom: 24px;
}
</style>

View File

@@ -1,19 +0,0 @@
<template>
<h2 class="sidebar-title sidebar-text">
<slot />
</h2>
</template>
<style scoped>
.sidebar-title {
font-weight: 700;
color: var(--vp-c-text-1);
}
.sidebar-text {
line-height: 24px;
font-size: 14px;
display: block;
transition: color 0.25s;
padding: 4px 0;
}
</style>

View File

@@ -12,7 +12,7 @@
.confetti-button:before,
.confetti-button:after {
position: absolute;
content: '';
content: "";
display: block;
width: 140%;
max-width: 160px;
@@ -41,16 +41,8 @@
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%;
background-size: 10% 10%, 20% 20%, 15% 15%, 20% 20%, 18% 18%, 10% 10%, 15% 15%,
10% 10%, 18% 18%;
}
.confetti-button:after {
@@ -63,14 +55,7 @@
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%;
background-size: 15% 15%, 20% 20%, 18% 18%, 20% 20%, 15% 15%, 10% 10%, 20% 20%;
}
.confetti-button.animate:before {
@@ -85,89 +70,35 @@
@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%;
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%;
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%;
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%;
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%,
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%,
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%;
background-size: 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%, 0% 0%;
}
}

View File

@@ -24,7 +24,7 @@ const links = computed(() => [
},
{
text: 'Github',
href: `${githubLink.value}`
href: `${githubLink.value}/issues`
},
{
text: 'Issues',

View File

@@ -1,23 +1,21 @@
import packageData from '../../../data/packageData.json';
import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
import fetchPackages from '../../../lib/fetchPackages';
import fetchPackages from "../../../lib/fetchPackages";
export default {
async load() {
const packages = await fetchPackages();
return {
packages: packages
.filter((p) => p?.name != null && p.name in packageData)
.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),
})).sort((a, b) => a.order - b.order),
thirdPartyPackages,
};
},
};
}
}

View File

@@ -1,8 +1,7 @@
<script setup lang="ts">
import { data } from './PackageList.data'
import {data} from './PackageList.data'
import GridSection from '../base/GridSection.vue'
import PackageListItem from "./PackageListItem.vue";
</script>
import PackageListItem from "./PackageListItem.vue";</script>
<template>
<GridSection
@@ -39,4 +38,33 @@ import PackageListItem from "./PackageListItem.vue";
.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>

View File

@@ -19,7 +19,7 @@ const props = defineProps<{
<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>
<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>

View File

@@ -40,4 +40,33 @@ import ShowcaseListItem from "./ShowcaseListItem.vue";
.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>

View File

@@ -1,23 +1,23 @@
import { onMounted, onUpdated, onUnmounted } from 'vue';
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils';
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
*/
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
*/
export function useActiveAnchor(container, marker) {
const setActiveLinkDebounced = throttleAndDebounce(setActiveLink, 100);
const onScroll = throttleAndDebounce(setActiveLink, 100);
let prevActiveLink = null;
onMounted(() => {
requestAnimationFrame(setActiveLink);
window.addEventListener('scroll', setActiveLinkDebounced);
window.addEventListener('scroll', onScroll);
});
onUpdated(() => {
// sidebar update means a route change
activateLink(location.hash);
});
onUnmounted(() => {
window.removeEventListener('scroll', setActiveLinkDebounced);
window.removeEventListener('scroll', onScroll);
});
function setActiveLink() {
const links = [].slice.call(container.value.querySelectorAll('.outline-link'));
@@ -59,18 +59,15 @@ export function useActiveAnchor(container, marker) {
activeLink.classList.add('active');
marker.value.style.top = activeLink.offsetTop + 5 + 'px';
marker.value.style.opacity = '1';
} else {
}
else {
marker.value.style.top = '33px';
marker.value.style.opacity = '0';
}
}
return {
setActiveLinkDebounced,
};
}
const PAGE_OFFSET = 128;
const PAGE_OFFSET = 64;
function getAnchorTop(anchor) {
return anchor.parentElement.offsetTop - PAGE_OFFSET;
@@ -78,7 +75,7 @@ function getAnchorTop(anchor) {
function isAnchorActive(index, anchor, nextAnchor) {
const scrollTop = window.scrollY;
if (index === 0 && scrollTop === 0) {
return [true, anchor.hash];
return [true, null];
}
if (scrollTop < getAnchorTop(anchor)) {
return [false, null];

View File

@@ -1,38 +1,25 @@
import { useRoute } from 'vitepress';
import { ref, inject, Ref, onMounted, watch } from 'vue';
import {
ref, inject, Ref
} from 'vue';
export const CATEGORY_VIEW_CONTEXT = Symbol('categoryView');
interface CategoryViewContext {
selectedCategory: Ref<string>;
categoryCounts: Ref<Record<string, number>>;
selectedCategory: Ref<string>
categoryCounts: Ref<Record<string, number>>
}
export const categoryViewContext = {
selectedCategory: ref(),
selectedCategory: ref(''),
categoryCounts: ref({}),
};
export function useCategoryView(): CategoryViewContext {
const context = inject<CategoryViewContext>(CATEGORY_VIEW_CONTEXT);
const route = useRoute();
if (!context) {
throw new Error('useCategoryView must be used with categoryView context');
}
onMounted(() => {
if (window.location.hash) {
context.selectedCategory.value = decodeURIComponent(window.location.hash.slice(1));
}
});
watch(route, (currentRoute) => {
if (currentRoute.path !== '/icons/categories') {
context.selectedCategory.value = '';
context.categoryCounts.value = {};
}
});
return context;
}

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