Merge branch 'main' into package/extract-build-logic

This commit is contained in:
Karsa
2026-07-14 17:02:55 +02:00
401 changed files with 5157 additions and 3028 deletions
+30 -1
View File
@@ -32,8 +32,37 @@ concurrency:
group: continuous-integration
jobs:
check-dispatch-gate:
if: github.repository == 'lucide-icons/lucide' &&
startsWith(github.event.head_commit.message, 'feat(icons)') &&
github.event_name != 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
RELEASE_ENVIRONMENT: ${{ steps.environment.outputs.RELEASE_ENVIRONMENT }}
steps:
- uses: actions/checkout@v6
- name: Fetch tags
run: git fetch --all --tags
- name: Check if latest tag was created today
id: environment
run: |
LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)
TAG_DATE=$(git log -1 --format=%cs "$LATEST_TAG")
TODAY=$(date +%Y-%m-%d)
if [ "$TAG_DATE" == "$TODAY" ]; then
echo "::warning::A release tag already exists for today ($TODAY): $LATEST_TAG. Manual approval will be required to proceed."
echo "RELEASE_ENVIRONMENT=Icon Release Gated" >> $GITHUB_OUTPUT
else
echo "RELEASE_ENVIRONMENT=Icon Release" >> $GITHUB_OUTPUT
fi
create-release:
if: github.repository == 'lucide-icons/lucide' && startsWith(github.event.head_commit.message, 'feat(icons)') && github.event_name != 'workflow_dispatch'
if: github.repository == 'lucide-icons/lucide'
needs: check-dispatch-gate
environment: ${{ needs.check-dispatch-gate.outputs.RELEASE_ENVIRONMENT }}
runs-on: ubuntu-latest
outputs:
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
@@ -33,7 +33,7 @@ jobs:
Lucide **does not accept** brand logos, and we do not plan to add them in the future. This is due to a combination of **legal restrictions**, **design consistency concerns**, and **practical maintenance reasons**.
[Click here to read our official statement about brand logos in Lucide.](./BRAND_LOGOS_STATEMENT.md)
[Click here to read our official statement about brand logos in Lucide.](https://github.com/lucide-icons/lucide/blob/main/BRAND_LOGOS_STATEMENT.md)
You can [search for similar issues.](https://github.com/lucide-icons/lucide/issues?q=is%3Aissue+${VALUE})
@@ -1,4 +1,4 @@
name: Pull request tags suggestions
name: Pull request metadata suggestions
on:
pull_request_target:
@@ -6,8 +6,8 @@ on:
- 'icons/*.json'
jobs:
pull-request-tags-suggestions:
name: Pull Request Tags Suggestions
pull-request-metadata-suggestions:
name: Pull Request Metadata Suggestions
runs-on: ubuntu-latest
permissions:
contents: read
@@ -29,7 +29,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Generate comment markup
run: node ./scripts/suggestTags.mts
run: node ./scripts/suggestMetaData.mts
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PULL_REQUEST_NUMBER: ${{ github.event.number }}
+2 -5
View File
@@ -20,9 +20,6 @@ permissions:
id-token: write # Required for OIDC
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
prepare:
if: github.repository == 'lucide-icons/lucide' && contains('["ericfennis", "karsa-mistmere", "jguddas"]', github.actor)
@@ -74,7 +71,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Set new version
run: pnpm --fail-if-no-match --filter ${{ matrix.package }} version --new-version ${{ needs.prepare.outputs.VERSION }} --no-git-tag-version
run: pnpm version ${{ needs.prepare.outputs.VERSION }} --filter ${{ matrix.package }} --fail-if-no-match --no-git-tag-version
- name: Build
run: pnpm --filter ${{ matrix.package }} build
@@ -108,7 +105,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Set new version
run: pnpm --filter lucide-static version --new-version ${{ needs.prepare.outputs.VERSION }} --no-git-tag-version
run: pnpm version ${{ needs.prepare.outputs.VERSION }} --filter lucide-static --no-git-tag-version
- name: Move Font
run: cp -r lucide-font packages/lucide-static/font
+42
View File
@@ -0,0 +1,42 @@
name: Weekly Repo Report
on:
workflow_dispatch:
# Run this workflow every Monday at 07:00 UTC
schedule:
- cron: '0 7 * * 1'
permissions:
contents: read
jobs:
weekly-summary:
runs-on: ubuntu-latest
steps:
- name: Set up Node.js environment
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install Copilot CLI
run: npm install -g @github/copilot
- name: Run the prompt
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
copilot -p "${{ vars.REPO_REPORT_PROMPT }} Write the details to summary.md" --allow-tool='shell(git:*)' --allow-tool=write --no-ask-user
cat summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Post report to Discord (private channel)
env:
DISCORD_WEBHOOK_URL: ${{ secrets.LUCIDE_DISCORD_WEBHOOK }}
run: |
jq -Rs '{
embeds: [{
title: "📊 Weekly Repo Report",
description: (.[0:4096]),
color: 5814783
}]
}' summary.md > payload.json
curl -sS --fail -X POST \
-H "Content-Type: application/json" \
-d @payload.json \
"$DISCORD_WEBHOOK_URL"
+1
View File
@@ -56,6 +56,7 @@ docs/.vitepress/data/categoriesData.json
docs/.vitepress/data/iconDetails
docs/.vitepress/data/relatedIcons.json
docs/.vitepress/data/brandStopwords.json
docs/.vitepress/data/iconPopularity
docs/.vercel
docs/.nitro
docs/public/og/**
+1 -1
View File
@@ -35,7 +35,7 @@ Lucide is an open-source icon library that provides 1600+ vector (svg) files for
| <img src="https://lucide.dev/framework-logos/js.svg" alt="JS logo" width="48"> | **`lucide`** | [![npm](https://img.shields.io/npm/v/lucide)](https://www.npmjs.com/package/lucide) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide) | [Docs](https://lucide.dev/guide/lucide) · [Source](./packages/lucide) |
| <img src="https://lucide.dev/framework-logos/react.svg" alt="React logo" width="48"> | **`lucide-react`** | [![npm](https://img.shields.io/npm/v/lucide-react)](https://www.npmjs.com/package/lucide-react) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-react) | [Docs](https://lucide.dev/guide/react) · [Source](./packages/lucide-react) |
| <img src="https://lucide.dev/framework-logos/vue.svg" alt="Vue logo" width="48"> | **`@lucide/vue`** | [![npm](https://img.shields.io/npm/v/@lucide/vue)](https://www.npmjs.com/package/@lucide/vue) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/vue) | [Docs](https://lucide.dev/guide/vue) · [Source](./packages/vue) |
| <img src="https://lucide.dev/framework-logos/svelte.svg" alt="Svelte logo" width="48"> | **`@lucide/svelte`** | [![npm](https://img.shields.io/npm/v/@lucide/svelte)](https://www.npmjs.com/package/@lucide/svelte) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/svelte) | [Docs](https://lucide.dev/guide/svelte) · [Source](./packages/@lucide/svelte) |
| <img src="https://lucide.dev/framework-logos/svelte.svg" alt="Svelte logo" width="48"> | **`@lucide/svelte`** | [![npm](https://img.shields.io/npm/v/@lucide/svelte)](https://www.npmjs.com/package/@lucide/svelte) | ![NPM Downloads](https://img.shields.io/npm/dw/@lucide/svelte) | [Docs](https://lucide.dev/guide/svelte) · [Source](./packages/svelte) |
| <img src="https://lucide.dev/framework-logos/solid.svg" alt="Solid logo" width="48"> | **`lucide-solid`** | [![npm](https://img.shields.io/npm/v/lucide-solid)](https://www.npmjs.com/package/lucide-solid) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-solid) | [Docs](https://lucide.dev/guide/solid) · [Source](./packages/lucide-solid) |
| <img src="https://lucide.dev/framework-logos/preact.svg" alt="Preact logo" width="48"> | **`lucide-preact`** | [![npm](https://img.shields.io/npm/v/lucide-preact)](https://www.npmjs.com/package/lucide-preact) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-preact) | [Docs](https://lucide.dev/guide/preact) · [Source](./packages/lucide-preact) |
| <img src="https://lucide.dev/framework-logos/react-native.svg" alt="React Native logo" width="48"> | **`lucide-react-native`** | [![npm](https://img.shields.io/npm/v/lucide-react-native)](https://www.npmjs.com/package/lucide-react-native) | ![NPM Downloads](https://img.shields.io/npm/dw/lucide-react-native) | [Docs](https://lucide.dev/guide/react-native) · [Source](./packages/lucide-react-native) |
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "../category.schema.json",
"title": "Navigation, Maps, and POIs",
"title": "Navigation & Places",
"icon": "compass"
}
+2 -1
View File
@@ -101,6 +101,7 @@ export default defineConfig({
},
{ text: 'Packages', link: '/packages' },
{ text: 'Showcase', link: '/showcase' },
{ text: 'Merch', link: 'https://merch.lucide.dev/' },
],
sidebar,
socialLinks: [
@@ -109,7 +110,7 @@ export default defineConfig({
],
footer: {
message: 'Released under the ISC License.',
copyright: `Copyright © ${new Date().getFullYear()} Lucide Contributors`,
copyright: `Copyright © ${new Date().getFullYear()} Lucide Icons`,
},
editLink: {
pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path',
+1 -1
View File
@@ -97,7 +97,7 @@
},
{
"name": "navigation",
"title": "Navigation, Maps, and POIs"
"title": "Navigation & Places"
},
{
"name": "notifications",
@@ -209,5 +209,24 @@
],
"source": "https://github.com/shx08/strapi-lucide-icons",
"documentation": "https://github.com/shx08/strapi-lucide-icons/blob/main/README.md"
},
{
"name": "lucide-lit",
"description": "Implementation of Lucide icons as Lit web components.",
"icon": "/framework-logos/lit.svg",
"shields": [
{
"alt": "Latest Stable Version",
"src": "https://img.shields.io/npm/v/@gyeonghokim/lucide-lit",
"href": "https://www.npmjs.com/package/@gyeonghokim/lucide-lit"
},
{
"alt": "Total Downloads",
"src": "https://img.shields.io/npm/dw/@gyeonghokim/lucide-lit",
"href": "https://www.npmjs.com/package/@gyeonghokim/lucide-lit"
}
],
"source": "https://github.com/GyeongHoKim/lucide-lit",
"documentation": "https://github.com/GyeongHoKim/lucide-lit/blob/main/README.md"
}
]
-7
View File
@@ -24,13 +24,6 @@ const getHeadConfig = ({
defer: '',
},
],
[
'script',
{
src: 'https://media.bitterbrains.com/main.js?from=LUCIDE&type=top',
async: 'true',
},
],
[
'meta',
{
+5
View File
@@ -72,6 +72,11 @@ export const reactNativeSidebar = [
link: '/guide/react-native/advanced/combining-icons',
desc: 'Combine multiple icons into one',
},
{
text: 'Optimizations',
link: '/guide/react-native/advanced/optimizations',
desc: 'Optimizing your app for performance',
},
],
},
{
@@ -11,6 +11,14 @@ const { isDark } = useData();
const emit = defineEmits(['update:modelValue']);
function limitHexInput(val: string) {
if (val.startsWith('#')) {
return val.slice(0, 9);
}
return val.slice(0, 8);
}
const value = computed({
get: () => {
if (props.modelValue == null || props.modelValue === 'currentColor') {
@@ -19,7 +27,7 @@ const value = computed({
return props.modelValue;
},
set: (val) => emit('update:modelValue', val),
set: (val) => emit('update:modelValue', limitHexInput(val)),
});
</script>
@@ -41,6 +49,7 @@ const value = computed({
class="color-input-text"
aria-label="Color picker input"
v-model="value"
maxlength="9"
placeholder="[default]"
/>
</div>
@@ -91,6 +100,7 @@ const value = computed({
background: transparent;
color: var(--vp-c-text-1);
font-size: 13px;
font-family: var(--vp-font-family-mono);
text-align: left;
border-radius: 8px;
cursor: text;
@@ -6,10 +6,10 @@ import SelectIcon from './SelectIcon.vue';
defineProps<{
id?: string;
items?: { name: string; route: string; icon: string; iconDark?: string }[];
items?: { name: string; value: string; icon?: string; iconDark?: string }[];
}>();
const selected = defineModel<{ name: string; icon: string; iconDark?: string }>();
const selected = defineModel<{ name: string; value: string; icon?: string; iconDark?: string }>();
</script>
<template>
@@ -19,14 +19,18 @@ const selected = defineModel<{ name: string; icon: string; iconDark?: string }>(
class="select-button"
:id="id"
>
<SelectIcon :item="selected" />
<slot name="start-icon">
<SelectIcon v-if="selected.icon" :item="selected" />
</slot>
<span class="select-text">{{ selected.name }}</span>
<span class="select-icon">
<Icon
:iconNode="chevronsUpDown"
class="chevron-icon"
aria-hidden="true"
/>
<slot name="end-icon">
<Icon
:iconNode="chevronsUpDown"
class="chevron-icon"
aria-hidden="true"
/>
</slot>
</span>
</ListboxButton>
@@ -43,8 +47,9 @@ const selected = defineModel<{ name: string; icon: string; iconDark?: string }>(
:value="item"
as="template"
>
<li :class="['select-option', { active, selected }]">
<SelectIcon :item="item" />
<SelectIcon v-if="item.icon" :item="item" />
<span :class="['option-text', { selected }]">{{ item.name }}</span>
<span
v-if="selected"
@@ -2,25 +2,28 @@
const { item } = defineProps<{
item: {
name: string;
icon: string;
icon?: string;
iconDark?: string;
};
}>();
</script>
<template>
<img
:src="item.icon"
:class="{ 'select-item-icon': true, light: item.iconDark }"
:alt="`${item.name} logo`"
loading="lazy"
/>
<img
v-if="item.iconDark"
:src="item.iconDark"
:alt="`${item.name} logo`"
class="select-item-icon dark"
/>
<div v-if="item.icon || item.iconDark">
<img
v-if="item.icon"
:src="item.icon"
:class="{ 'select-item-icon': true, light: item.iconDark }"
:alt="`${item.name} logo`"
loading="lazy"
/>
<img
v-if="item.iconDark"
:src="item.iconDark"
:alt="`${item.name} logo`"
class="select-item-icon dark"
/>
</div>
</template>
<style scoped>
@@ -1,90 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vitepress';
import { computed } from 'vue';
const router = useRouter();
const frameworks = [
{
ariaLabel: 'Get JavaScript certificates from certificates.dev',
image: '/certificates-dev/js-certificates-dev.jpg',
href: 'https://certificates.dev/javascript?friend=LUCIDE',
routes: [
'/guide/lucide/',
'/guide/svelte/',
'/guide/solid/',
'/guide/astro/',
'/guide/static/'
],
},
{
ariaLabel: 'Get React certificates from certificates.dev',
image: '/certificates-dev/react-certificates-dev.jpg',
href: 'https://certificates.dev/react?friend=LUCIDE',
routes: [
'/guide/react/',
'/guide/react-native/',
'/guide/preact/',
],
},
{
ariaLabel: 'Get Vue certificates from certificates.dev',
href: 'https://certificates.dev/vuejs?friend=LUCIDE',
image: '/certificates-dev/vue-certificates-dev.jpg',
routes: [
'/guide/vue/',
],
},
{
ariaLabel: 'Get Angular certificates from certificates.dev',
href: 'https://certificates.dev/angular?friend=LUCIDE',
image: '/certificates-dev/angular-certificates-dev.jpg',
routes: [
'/guide/angular/',
],
}
];
const selected = computed(() => {
const current = frameworks.find(({ routes }) => {
return routes.some(route => router.route.path.startsWith(route));
});
return current || frameworks[0];
});
</script>
<template>
<a class="certificates-link" :href="selected.href" :aria-label="selected.ariaLabel" target="_blank" rel="noopener noreferrer">
<img :src="selected.image" :alt="selected.ariaLabel" />
</a>
</template>
<style scoped>
.certificates-link {
/* padding: 20px; */
border-radius: 12px;
min-height: 100px;
text-align: center;
line-height: 18px;
font-size: 12px;
font-weight: 500;
background-color: var(--vp-carbon-ads-bg-color);
margin-bottom: 20px;
overflow: hidden;
}
.certificates-link img {
width: 100%;
height: 100%;
max-width: 100%;
border-radius: 6px;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.certificates-link:hover img {
transform: scale(1.05);
opacity: 0.9;
}
</style>
@@ -11,49 +11,49 @@ const { page } = useData();
const router = useRouter();
const frameworks = [
{ name: 'Vanilla', icon: '/framework-logos/js.svg', route: '/guide/lucide/' },
{ name: 'React', icon: '/framework-logos/react.svg', route: '/guide/react/' },
{ name: 'Vue', icon: '/framework-logos/vue.svg', route: '/guide/vue/' },
{ name: 'Svelte', icon: '/framework-logos/svelte.svg', route: '/guide/svelte/' },
{ name: 'Solid', icon: '/framework-logos/solid.svg', route: '/guide/solid/' },
{ name: 'Angular', icon: '/framework-logos/angular.svg', route: '/guide/angular/' },
{ name: 'Preact', icon: '/framework-logos/preact.svg', route: '/guide/preact/' },
{ name: 'Vanilla', icon: '/framework-logos/js.svg', value: '/guide/lucide/' },
{ name: 'React', icon: '/framework-logos/react.svg', value: '/guide/react/' },
{ name: 'Vue', icon: '/framework-logos/vue.svg', value: '/guide/vue/' },
{ name: 'Svelte', icon: '/framework-logos/svelte.svg', value: '/guide/svelte/' },
{ name: 'Solid', icon: '/framework-logos/solid.svg', value: '/guide/solid/' },
{ name: 'Angular', icon: '/framework-logos/angular.svg', value: '/guide/angular/' },
{ name: 'Preact', icon: '/framework-logos/preact.svg', value: '/guide/preact/' },
{
name: 'React Native',
icon: '/framework-logos/react-native.svg',
route: '/guide/react-native/',
value: '/guide/react-native/',
},
{
name: 'Astro',
icon: '/framework-logos/astro.svg',
iconDark: '/framework-logos/astro-dark.svg',
route: '/guide/astro/',
value: '/guide/astro/',
},
{ name: 'Static', icon: '/framework-logos/svg.svg', route: '/guide/static/' },
{ name: 'Static', icon: '/framework-logos/svg.svg', value: '/guide/static/' },
];
const fallbackFramework = useLocalStorage('lucide-docs-fallback-framework', frameworks[1]);
const selected = computed(() => {
const current = frameworks.find(({ route }) => {
const current = frameworks.find(({ value }) => {
if (router.route.path?.startsWith?.('/guide')) {
const [, , framework] = router.route.path.split('/');
const [, frameWorkRoute] = route.split('/').filter(Boolean);
const [, frameWorkRoute] = value.split('/').filter(Boolean);
return framework === frameWorkRoute;
}
return router.route.path.split('/').slice(0, 3).join('/') === route;
return router.route.path.split('/').slice(0, 3).join('/') === value;
});
return current || fallbackFramework.value;
return current || (fallbackFramework.value?.value ? fallbackFramework.value : frameworks[0]);
});
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; route: string }) {
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; value: string }) {
fallbackFramework.value = item;
if (item.route !== router.route.path) {
const likeRoute = router.route.path.replace(selected.value.route, item.route);
if (item.value !== router.route.path) {
const likeRoute = router.route.path.replace(selected.value.value, item.value);
const hasRoute = sidebar[item.route]?.some((section) =>
const hasRoute = sidebar[item.value]?.some((section) =>
section?.items?.some(({ link }) => link === likeRoute),
);
@@ -62,7 +62,7 @@ function onSelectFramework(item: { name: string; icon: string; iconDark?: string
return;
}
router.go(item.route);
router.go(item.value);
}
}
</script>
@@ -85,12 +85,12 @@ function onSelectFramework(item: { name: string; icon: string; iconDark?: string
/>
</div>
<VPSidebarGroup
:key="selected.route"
:key="selected.value"
v-if="
page?.relativePath?.startsWith?.('guide') &&
!page?.relativePath?.startsWith?.(selected.route.substring(1))
!page?.relativePath?.startsWith?.(selected.value.substring(1))
"
:items="sidebar[selected.route]"
:items="sidebar[selected.value]"
/>
</template>
@@ -188,7 +188,7 @@ const deprecatedTitle = computed(() => {
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: currentColor transparent; /* foreground background */
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
}
.horizontal-scroller::-webkit-scrollbar {
width: 0;
@@ -44,8 +44,12 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categoriesMap } = useFetchCategories();
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categoriesMap,
isFetching: isFetchingCategories,
} = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -117,6 +121,9 @@ const categoriesList = computed(() => {
}, []);
});
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && (tags.value == null || categoriesMap.value == null),
);
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
itemHeight: ICON_SIZE + ICON_GRID_GAP,
@@ -135,15 +142,21 @@ onMounted(() => {
useEventListener(window, 'scroll', containerProps.onScroll);
});
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
}
if (categoriesMap.value == null) {
fetchCategories();
if (categoriesMap.value == null && !isFetchingCategories.value) {
void fetchCategories();
}
}
watch(searchQuery, (searchString) => {
if (searchString !== '') {
loadSearchMetadata();
}
});
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
@@ -156,11 +169,11 @@ function handleCloseDrawer() {
if (searchQueryDebounced.value) {
url.searchParams.set('search', searchQueryDebounced.value);
}
if (selectedCategory.value) {
url.hash = selectedCategory.value;
}
window.history.pushState({}, '', url);
}
</script>
@@ -169,6 +182,7 @@ function handleCloseDrawer() {
<div
ref="overviewEl"
class="overview-container"
:class="{ 'icon-drawer-open': activeIconName }"
>
<StickyBar class="category-search">
<InputSearch
@@ -177,11 +191,11 @@ function handleCloseDrawer() {
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
@focus="onFocusSearchInput"
@focus="loadSearchMetadata"
/>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults"
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"
@@ -4,6 +4,7 @@ import type { IconEntity } from '../../types';
import { useElementSize, useEventListener, useVirtualList } from '@vueuse/core';
import { useRoute } from 'vitepress';
import IconGrid from './IconGrid.vue';
import Select from '../base/Select.vue';
import InputSearch from '../base/InputSearch.vue';
import useSearch from '../../composables/useSearch';
import useSearchInput from '../../composables/useSearchInput';
@@ -14,9 +15,26 @@ import useFetchCategories from '../../composables/useFetchCategories';
import chunkArray from '../../utils/chunkArray';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
import Icon from '@lucide/vue/src/Icon';
import { listSortDescending } from '~/.vitepress/data/iconNodes';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const SORTING = [
{
name: 'Popularity',
value: 'popularity',
},
{
name: 'Release date',
value: 'release-date',
},
{
name: 'Name',
value: 'name',
},
]
const initialGridItems = computed(() => {
if (containerWidth.value === 0) return 120;
@@ -32,9 +50,14 @@ const props = defineProps<{
}>();
const activeIconName = ref(null);
const selectedSort = ref(SORTING[0])
const { execute: fetchTags, data: tags } = useFetchTags();
const { execute: fetchCategories, data: categories } = useFetchCategories();
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categories,
isFetching: isFetchingCategories,
} = useFetchCategories();
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -43,12 +66,29 @@ const columnSize = computed(() => {
return Math.floor(containerWidth.value / (ICON_SIZE + ICON_GRID_GAP));
});
const sortedIcons = computed(() => {
switch (selectedSort.value.value) {
case 'popularity':
return [...props.icons].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
case 'release-date':
return [...props.icons].sort((a, b) => {
const aDate = a.createdRelease?.date ? new Date(a.createdRelease.date).getTime() : 0;
const bDate = b.createdRelease?.date ? new Date(b.createdRelease.date).getTime() : 0;
return bDate - aDate;
});
case 'name':
return [...props.icons].sort((a, b) => a.name.localeCompare(b.name));
default:
return props.icons;
}
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
return sortedIcons.value;
}
return props.icons.map((icon) => {
return sortedIcons.value.map((icon) => {
const iconTags = tags.value[icon.name];
const iconCategories = categories.value?.[icon.name] ?? [];
@@ -68,11 +108,15 @@ const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'aliases', weight: 8 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
]);
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && (tags.value == null || categories.value == null),
);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
@@ -98,15 +142,21 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
function onFocusSearchInput() {
if (tags.value == null) {
fetchTags();
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
}
if (categories.value == null) {
fetchCategories();
if (categories.value == null && !isFetchingCategories.value) {
void fetchCategories();
}
}
watch(searchQuery, (searchString) => {
if (searchString !== '') {
loadSearchMetadata();
}
});
const NoResults = defineAsyncComponent(() => import('./NoResults.vue'));
const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay.vue'));
@@ -124,7 +174,7 @@ function handleCloseDrawer() {
if (searchQueryDebounced.value) {
url.searchParams.set('search', searchQueryDebounced.value);
}
window.history.pushState({}, '', url);
}
</script>
@@ -133,6 +183,7 @@ function handleCloseDrawer() {
<div
ref="overviewEl"
class="overview-container"
:class="{ 'icon-drawer-open': activeIconName }"
>
<StickyBar>
<InputSearch
@@ -141,11 +192,25 @@ function handleCloseDrawer() {
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@focus="onFocusSearchInput"
@focus="loadSearchMetadata"
/>
<Select
id="sort-select"
:items="SORTING"
v-model="selectedSort"
>
<template #start-icon>
<Icon
:iconNode="listSortDescending"
class="chevron-icon"
aria-hidden="true"
/>
</template>
</Select>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults"
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"
@@ -73,13 +73,11 @@ const customizingActive = computed(() => {
id="icon-color"
label="Color"
>
<template #display>
<ColorPicker
v-model="color"
id="icon-color"
class="color-picker"
/>
</template>
<ColorPicker
v-model="color"
id="icon-color"
class="color-picker"
/>
</InputField>
<InputField
@@ -161,6 +159,6 @@ const customizingActive = computed(() => {
}
.color-picker {
margin-left: auto;
width: 100%;
}
</style>
@@ -15,6 +15,50 @@
margin-bottom: 32px;
background: var(--vp-c-bg);
box-shadow: 0 16px 24px var(--vp-c-bg);
gap: 16px;
}
.search-bar :deep(.select-button) {
padding: 11px 14px;
background-color: var(--vp-c-bg-alt);
color: var(--vp-c-text-2);
flex-shrink: 0;
flex: 1;
font-weight: 500;
cursor: pointer;
}
.search-bar :deep(.select-button):hover,
.search-bar :deep(.select-button):focus {
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-alt);
}
.search-bar :deep(.select-options) {
min-width: 160px;
}
.search-bar :deep(.select-option) {
padding-right: 36px;
padding-left: 8px;
}
.search-bar :deep(.select-icon) {
display: none;
}
.search-bar :deep(.select-button) .select-text {
display: none;
}
@media (min-width: 640px) {
.search-bar :deep(.select-button) {
padding: 11px 16px 11px 16px;
}
.search-bar :deep(.select-button) .select-text {
display: block;
}
}
@media (min-width: 960px) {
@@ -20,16 +20,6 @@ const useSearch = <T>(
return index.value.search(query.value).map((result) => result.item);
}
if (keys.length !== 0) {
const mainKey = keys[0].name;
return collection.value.sort((a, b) => {
const aString = a[mainKey as keyof T] as string;
const bString = b[mainKey as keyof T] as string;
return aString.localeCompare(bString);
});
}
return collection.value;
});
-2
View File
@@ -12,7 +12,6 @@ import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCat
import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs';
import FrameworkSelect from './components/guide/FrameworkSelect.vue';
import SnackPlayer from './components/editors/SnackPlayer.vue';
import CertificatesLink from './components/guide/CertificatesLink.vue';
const theme: Partial<Theme> = {
extends: DefaultTheme,
@@ -23,7 +22,6 @@ const theme: Partial<Theme> = {
'sidebar-nav-after': () => h(IconsSidebarNavAfter),
'home-hero-image': () => h(HomeHeroIconsCard),
'home-hero-actions-after': () => h(HomeHeroAfter),
'aside-ads-before': () => h(CertificatesLink),
// Keep this here for now, we might want to add more things to the top bar in the future
// 'layout-top': () => h(LayoutTop),
});
+27 -12
View File
@@ -67,6 +67,10 @@
animation: .3s transition 0s ease;
} */
html {
scrollbar-gutter: stable;
}
.VPHome {
overflow-x: hidden;
}
@@ -117,6 +121,10 @@
padding-left: 0;
}
.overview-container.icon-drawer-open {
padding-bottom: 288px;
}
.VPHomeHero .container {
flex-direction: column-reverse;
}
@@ -144,14 +152,6 @@
.VPHomeHero .container {
flex-direction: row;
}
html.has-bb-banner {
--vp-layout-top-height: 72px;
}
html.has-bb-banner {
--Dark-Background: #161618;
}
}
.VPNavBarHamburger .container > span {
@@ -163,8 +163,23 @@
max-width: 480px;
}
html:not(.has-bb-banner) #bb-banner {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
.VPSidebar {
/* Hide Scrollbar */
-ms-overflow-style: none;
scrollbar-width: none;
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
}
.VPSidebar::-webkit-scrollbar {
width: 0;
display: none;
}
.VPSidebar::-webkit-scrollbar-track {
background: transparent;
}
.VPSidebar::-webkit-scrollbar-thumb {
background: transparent;
border: none;
}
+1
View File
@@ -18,6 +18,7 @@ export interface IconEntity extends IconMetaData {
externalLibrary?: ExternalLibs;
createdRelease?: Release;
changedRelease?: Release;
popularity?: number;
}
export interface Category {
+1 -1
View File
@@ -1,4 +1,4 @@
import { PageData } from 'vitepress';
import { type PageData } from 'vitepress';
import getStructuredData from './getStructuredData';
import { createGeneralOGImage, createIconOGImage } from './createOGImage';
+64 -28
View File
@@ -12,19 +12,23 @@ Implementation of the Lucide icon library for web applications.
::: code-group
```sh [pnpm]
pnpm add lucide@next
pnpm add lucide
```
```sh [yarn]
yarn add lucide@next
yarn add lucide
```
```sh [npm]
npm install lucide@next
npm install lucide
```
```sh [bun]
bun add lucide@next
bun add lucide
```
```sh [deno]
deno add lucide
```
:::
@@ -38,19 +42,23 @@ Implementation of the Lucide icon library for React applications.
::: code-group
```sh [pnpm]
pnpm add lucide-react@next
pnpm add lucide-react
```
```sh [yarn]
yarn add lucide-react@next
yarn add lucide-react
```
```sh [npm]
npm install lucide-react@next
npm install lucide-react
```
```sh [bun]
bun add lucide-react@next
bun add lucide-react
```
```sh [deno]
deno add lucide-react
```
:::
@@ -80,6 +88,10 @@ npm install @lucide/vue
bun add @lucide/vue
```
```sh [deno]
deno add @lucide/vue
```
:::
For more details, see the [documentation](./vue/index.md).
@@ -91,19 +103,23 @@ Implementation of the Lucide icon library for Svelte applications.
::: code-group
```sh [pnpm]
pnpm add @lucide/svelte@next
pnpm add @lucide/svelte
```
```sh [yarn]
yarn add @lucide/svelte@next
yarn add @lucide/svelte
```
```sh [npm]
npm install @lucide/svelte@next
npm install @lucide/svelte
```
```sh [bun]
bun add @lucide/svelte@next
bun add @lucide/svelte
```
```sh [deno]
deno add @lucide/svelte
```
:::
> `@lucide/svelte` is only for Svelte 5, for Svelte 4 use the `lucide-svelte` package.
@@ -117,19 +133,23 @@ Implementation of the Lucide icon library for Solid applications.
::: code-group
```sh [pnpm]
pnpm add lucide-solid@next
pnpm add lucide-solid
```
```sh [yarn]
yarn add lucide-solid@next
yarn add lucide-solid
```
```sh [npm]
npm install lucide-solid@next
npm install lucide-solid
```
```sh [bun]
bun add lucide-solid@next
bun add lucide-solid
```
```sh [deno]
deno add lucide-solid
```
:::
@@ -158,6 +178,10 @@ npm install @lucide/angular
bun add @lucide/angular
```
```sh [deno]
deno add @lucide/angular
```
:::
For more details, see the [documentation](./angular/index.md).
@@ -169,19 +193,23 @@ Implementation of the Lucide icon library for preact applications.
::: code-group
```sh [pnpm]
pnpm add lucide-preact@next
pnpm add lucide-preact
```
```sh [yarn]
yarn add lucide-preact@next
yarn add lucide-preact
```
```sh [npm]
npm install lucide-preact@next
npm install lucide-preact
```
```sh [bun]
bun add lucide-preact@next
bun add lucide-preact
```
```sh [deno]
deno add lucide-preact
```
@@ -196,19 +224,23 @@ Implementation of the Lucide icon library for Astro applications.
::: code-group
```sh [pnpm]
pnpm add @lucide/astro@next
pnpm add @lucide/astro
```
```sh [yarn]
yarn add @lucide/astro@next
yarn add @lucide/astro
```
```sh [npm]
npm install @lucide/astro@next
npm install @lucide/astro
```
```sh [bun]
bun add @lucide/astro@next
bun add @lucide/astro
```
```sh [deno]
deno add @lucide/astro
```
:::
@@ -222,19 +254,23 @@ Implementation of the Lucide icon library for multiple usages that like to use:
::: code-group
```sh [pnpm]
pnpm add lucide-static@next
pnpm add lucide-static
```
```sh [yarn]
yarn add lucide-static@next
yarn add lucide-static
```
```sh [npm]
npm install lucide-static@next
npm install lucide-static
```
```sh [bun]
bun add lucide-static@next
bun add lucide-static
```
```sh [deno]
deno add lucide-static
```
:::
@@ -0,0 +1,56 @@
---
title: Optimizations - React Native
description: Learn how to optimize the bundle size of your React Native and Expo web exports by tree-shaking Lucide icons.
---
# Optimizations
By default you import icons from the `lucide-react-native` entry point:
```jsx
import { Camera } from 'lucide-react-native';
```
This is convenient, but it relies on the bundler to _tree-shake_ the entry point (a "barrel" file that re-exports every icon) so that only the icons you actually use end up in your bundle.
On native platforms this is usually fine. On **web exports** it can be a problem, because the Metro bundler is currently not very good at tree-shaking barrel imports and may include **all** icons in your final bundle.
## The Metro tree-shaking limitation
When you build for web with Expo (`expo export --platform web`), Metro is used to bundle your JavaScript. Metro bundles modules on-demand and, by default, does not remove unused exports from a barrel file.
This means an import like the one below can pull in the entire icon set, even though only a single icon is rendered:
```jsx
// May bundle every Lucide icon on web exports
import { Camera } from 'lucide-react-native';
```
Expo documents an experimental optimization for [React Native web imports](https://docs.expo.dev/guides/tree-shaking/#react-native-web-imports) and for [removing unused imports and exports](https://docs.expo.dev/guides/tree-shaking/#remove-unused-imports-and-exports). In practice this tree-shaking does **not** currently work reliably for `lucide-react-native` barrel imports, so the unused icons are not removed from web exports.
## Recommended: import icons individually
To guarantee that only the icons you use are bundled — regardless of the bundler's tree-shaking capabilities — import each icon directly from its own module:
```jsx
import Camera from 'lucide-react-native/icons/camera';
// Usage
const App = () => {
return <Camera />;
};
export default App;
```
Because each icon lives in its own file, the bundler only ever includes the icons you explicitly import. This keeps your web export small without depending on Metro's experimental tree-shaking.
The icon module name is the [kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) version of the icon name. For example, the `ArrowRight` icon is imported from `lucide-react-native/icons/arrow-right`:
```jsx
import ArrowRight from 'lucide-react-native/icons/arrow-right';
```
::: tip
Individual icon imports work identically on native and web, so you can safely use this style across your whole project to keep bundles as small as possible on every platform.
:::
+4 -6
View File
@@ -15,24 +15,22 @@ Make sure you have a Svelte environment set up. If you don't have one yet, you c
## Installation
<!-- TODO: v1 Remove After full release -->
::: code-group
```sh [pnpm]
pnpm install @lucide/svelte@next
pnpm install @lucide/svelte
```
```sh [yarn]
yarn add @lucide/svelte@next
yarn add @lucide/svelte
```
```sh [npm]
npm install @lucide/svelte@next
npm install @lucide/svelte
```
```sh [bun]
bun add @lucide/svelte@next
bun add @lucide/svelte
```
:::
+12 -2
View File
@@ -1,9 +1,19 @@
import iconNodes from '../.vitepress/data/iconNodes';
import * as iconDetails from '../.vitepress/data/iconDetails';
export default {
async load() {
return {
icons: Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode })),
icons: Object.entries(iconDetails).map(
([, { name, iconNode, popularity, createdRelease, aliases = [] }]) => ({
name,
iconNode,
popularity: popularity?.count ?? 0,
aliases: aliases
.map((alias) => (typeof alias === 'string' ? alias : alias?.name))
.filter((alias): alias is string => Boolean(alias)),
createdRelease,
}),
),
};
},
};
+10 -9
View File
@@ -9,14 +9,15 @@
"docs:build": "pnpm run /^prebuild:.*/ && vitepress build",
"docs:preview": "vitepress preview",
"build:docs": "vitepress build",
"prebuild:iconNodes": "node ./scripts/writeIconNodes.mjs",
"prebuild:metaJson": "node ./scripts/writeIconMetaIndex.mjs",
"prebuild:releaseJson": "node ./scripts/writeReleaseMetadata.mjs",
"prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mjs",
"prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mjs",
"prebuild:iconDetails": "node ./scripts/writeIconDetails.mjs",
"prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mjs",
"postbuild:vercelJson": "node ./scripts/writeVercelOutput.mjs",
"prebuild:iconNodes": "node ./scripts/writeIconNodes.mts",
"prebuild:metaJson": "node ./scripts/writeIconMetaIndex.mts",
"prebuild:releaseJson": "node ./scripts/writeReleaseMetadata.mts",
"prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mts",
"prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mts",
"prebuild:iconDetails": "node ./scripts/writeIconDetails.mts",
"prebuild:iconPopularity": "node ./scripts/writeiconPopularity.mts",
"prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mts",
"postbuild:vercelJson": "node ./scripts/writeVercelOutput.mts",
"dev": "npx nitropack dev",
"prebuild:api": "npx nitropack prepare",
"build:api": "npx nitropack build",
@@ -38,7 +39,7 @@
"vitepress": "^1.6.4",
"vitepress-plugin-group-icons": "^1.6.5",
"vitepress-plugin-llms": "^1.11.1",
"markdown-it": "^14.1.0",
"markdown-it": "^14.2.0",
"markdown-it-container": "^4.0.0",
"semver": "^7.7.3",
"sharp": "^0.34.5",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 200" fill="none">
<path fill="#00e8ff" d="M40 120l20-60l90 90l-30 50l-40-40h-20" />
<path fill="#283198" d="M80 160 L80 80 L120 40 L 120 120 M0 160 L40 200 L40 120 L20 120" />
<path fill="#324fff" d="M40 120v-80l40-40v80M120 200v-80l40-40v80M0 160v-80l40 40" />
<path fill="#0ff" d="M40 200v-80l40 40" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

@@ -26,6 +26,7 @@ const writeIconFiles = icons.map(async (iconFileName) => {
import iconNode from '../iconNodes/${iconName}.node.json'
import metaData from '../../../../icons/${iconName}.json'
import releaseData from '../releaseMetadata/${iconName}.json'
import popularity from '../iconPopularity/${iconName}.json'
const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData
@@ -37,6 +38,7 @@ const iconDetails = {
categories,
aliases,
deprecated,
popularity,
deprecationReason,
toBeRemovedInVersion,
...releaseData,
@@ -42,7 +42,7 @@ const getRelatedIcons = (currentIcon, icons) => {
};
const iconsMetaDataPromises = svgFiles.map(async (iconName) => {
const metaDataFileContent = await fs.promises.readFile(`../icons/${iconName}`);
const metaDataFileContent = await fs.promises.readFile(`../icons/${iconName}`, 'utf-8');
const metaData = JSON.parse(metaDataFileContent);
const name = iconName.replace('.json', '');
@@ -70,10 +70,10 @@ const comparisonsPromises = tags.map(async (tag, index) => {
};
});
const comparisons = await Promise.all(comparisonsPromises);
const comparisons = (await Promise.all(comparisonsPromises)).filter((comparison) => comparison != null && Object.keys(comparison.iconFiles).length > 0);
const newReleaseMetaData = {};
comparisons.forEach(({ tag, iconFiles, date } = {}) => {
comparisons.forEach(({ tag, iconFiles, date }) => {
if (tag == null) return;
iconFiles.forEach(({ status, file, renamedFile }) => {
@@ -11,7 +11,7 @@ const iconMetaData = await getIconMetaData(path.resolve(scriptDir, '../../icons'
const iconAliasesRedirectRoutes = Object.entries(iconMetaData)
.filter(([, { aliases }]) => aliases?.length)
.map(([iconName, { aliases }]) => {
const aliasRouteMatches = aliases.map((alias) => alias.name).join('|');
const aliasRouteMatches = aliases.map((alias) => typeof alias === 'string' ? alias : alias?.name).join('|');
return {
src: `^/icons/(${aliasRouteMatches})$`,
+104
View File
@@ -0,0 +1,104 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import fs from 'fs';
import path from 'path';
import { loadEnvFile } from 'node:process';
import { readSvgDirectory } from '@lucide/helpers';
const currentDir = process.cwd();
const ICONS_DIR = path.resolve(currentDir, '../icons');
const iconPopularityDirectory = path.resolve(currentDir, '.vitepress/data', 'iconPopularity');
try {
// Load environment variables from .env file, if it exists.
loadEnvFile(`${currentDir}/.env`);
} catch (error) {}
if (fs.existsSync(iconPopularityDirectory)) {
fs.rmSync(iconPopularityDirectory, { recursive: true, force: true });
}
if (!fs.existsSync(iconPopularityDirectory)) {
fs.mkdirSync(iconPopularityDirectory);
}
const iconJsonFiles = await readSvgDirectory(ICONS_DIR, '.json');
const iconNames = iconJsonFiles.map((file) => path.basename(file, '.json'));
const defaultIconNamePopularity: Record<string, number> = Object.fromEntries(
iconNames.map((iconName) => [iconName, 0])
);
interface AnalyticsResponse {
results: {
dimensions: [route: string];
metrics: [count: number];
}[];
}
const writeIconPopularity = async (iconVisitCounts: Record<string, number>) => {
const writePopularityPromises = Object.entries(iconVisitCounts).map(([iconName, count]) => {
const location = path.resolve(iconPopularityDirectory, `${iconName}.json`);
return fs.promises.writeFile(location, JSON.stringify({ count }, null, 2), 'utf-8');
});
await Promise.all(writePopularityPromises);
console.log('Successfully written icon popularity data for', writePopularityPromises.length, 'icons.');
};
if (process.env.LUCIDE_ANALYTICS_TOKEN === undefined) {
console.warn('LUCIDE_ANALYTICS_TOKEN environment variable is not set. Writing default icon popularity data.');
await writeIconPopularity(defaultIconNamePopularity);
process.exit(0);
}
try {
const response = await fetch('https://analytics.lucide.dev/api/v2/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.LUCIDE_ANALYTICS_TOKEN}`,
},
body: JSON.stringify({
"site_id": "lucide.dev",
"metrics": [
"visitors"
],
"dimensions": [
"visit:entry_page"
],
"date_range": "12mo"
}),
})
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
const analyticsData: AnalyticsResponse = await response.json();
const iconVisitCounts = analyticsData.results.reduce((acc, { dimensions, metrics }) => {
const [route] = dimensions;
const [count] = metrics;
if (!route.startsWith('/icons')) {
return acc
}
// Only match routes that start with /icons/ and end with an icon name, e.g. /icons/activity, /icons/activity/ or /icons/activity?ref=homepage
const match = route.match(/^\/icons\/([^\/\?]+)(?:[\/\?]|$)/);
if (match) {
const iconName = match[1];
if (iconNames.includes(iconName)) {
acc[iconName] = (acc[iconName] || 0) + count;
}
}
return acc;
}, defaultIconNamePopularity);
await writeIconPopularity(iconVisitCounts);
} catch (error) {
console.error('Error fetching analytics data:', error);
await writeIconPopularity(defaultIconNamePopularity);
}
+2 -2
View File
@@ -40,7 +40,7 @@
}
},
"dependentRequired": {
"deprecated": ["deprecationReason", "toBeRemovedInVersion"],
"deprecated": ["deprecationReason"],
"deprecationReason": ["deprecated"],
"toBeRemovedInVersion": ["deprecated"]
}
@@ -134,7 +134,7 @@
}
},
"dependentRequired": {
"deprecated": ["deprecationReason", "toBeRemovedInVersion"],
"deprecated": ["deprecationReason"],
"deprecationReason": ["deprecated"],
"toBeRemovedInVersion": ["deprecated"]
},
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"ahtohbi4",
"jamiemlaw",
"karsa-mistmere",
"jguddas"
],
"use-cases": [
"marking content as sponsored",
"linking to an advertise with us page"
],
"tags": [
"advert",
"affiliate",
"brand",
"campaign",
"commercial",
"marketing",
"monetize",
"paid",
"partner",
"promo",
"sponsor"
],
"categories": [
"multimedia",
"notifications"
]
}
+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13H6" />
<path d="M10 15v-4a2 2 0 0 0-4 0v4" />
<path d="M14 14.5a.5.5 0 0 0 .5.5h1a2.5 2.5 0 0 0 2.5-2.5v-1A2.5 2.5 0 0 0 15.5 9h-1a.5.5 0 0 0-.5.5z" />
<rect width="20" height="14" x="2" y="5" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 434 B

+1 -2
View File
@@ -23,8 +23,7 @@
{
"name": "alarm-check",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -19,8 +19,7 @@
{
"name": "alarm-minus",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -19,8 +19,7 @@
{
"name": "alarm-plus",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+3 -3
View File
@@ -25,8 +25,8 @@
{
"name": "arrow-down-01",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-down-01"
]
}
+3 -3
View File
@@ -25,8 +25,8 @@
{
"name": "arrow-down-10",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-down-10"
]
}
+3 -3
View File
@@ -25,8 +25,8 @@
{
"name": "arrow-down-az",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-down-az"
]
}
+1 -2
View File
@@ -25,8 +25,7 @@
{
"name": "sort-desc",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+3 -3
View File
@@ -26,8 +26,8 @@
{
"name": "arrow-down-za",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-down-za"
]
}
+3 -3
View File
@@ -25,8 +25,8 @@
{
"name": "arrow-up-01",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-up-01"
]
}
+3 -3
View File
@@ -25,8 +25,8 @@
{
"name": "arrow-up-10",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-up-10"
]
}
+3 -3
View File
@@ -25,8 +25,8 @@
{
"name": "arrow-up-az",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-up-az"
]
}
+1 -2
View File
@@ -26,8 +26,7 @@
{
"name": "sort-asc",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+3 -3
View File
@@ -26,8 +26,8 @@
{
"name": "arrow-up-za",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"arrow-up-za"
]
}
+3 -3
View File
@@ -16,8 +16,8 @@
{
"name": "axis-3-d",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
}
"deprecated": true
},
"axis-3-d"
]
}
+1 -2
View File
@@ -8,8 +8,7 @@
{
"name": "verified",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
],
"use-cases": [],
+1 -2
View File
@@ -19,8 +19,7 @@
{
"name": "badge-help",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+33
View File
@@ -0,0 +1,33 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"mfjramirezf"
],
"use-cases": [
"confirm a paid product",
"confirm a payment method was added successfully"
],
"tags": [
"banknote",
"bill",
"currency",
"payment",
"money",
"finance",
"document",
"verification",
"tick",
"verified",
"accepted",
"done",
"complete",
"found",
"paid",
"task",
"check",
"success"
],
"categories": [
"finance"
]
}
+17
View File
@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11.748 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4.875" />
<path d="m16 19 2 2 4-4" />
<path d="M18 12h.01" />
<path d="M6 12h.01" />
<circle cx="12" cy="12" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 405 B

+1 -2
View File
@@ -36,8 +36,7 @@
{
"name": "between-horizonal-end",
"deprecated": true,
"deprecationReason": "alias.typo",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.typo"
}
]
}
+1 -2
View File
@@ -36,8 +36,7 @@
{
"name": "between-horizonal-start",
"deprecated": true,
"deprecationReason": "alias.typo",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.typo"
}
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "../icon.schema.json",
"contributors": [
"Patolord"
],
"use-cases": [
"indicate injury or fracture status in health/fitness tracking",
"show an error or failed state in healthcare UI flows"
],
"tags": [
"bone",
"fracture",
"injury",
"orthopedic",
"medical",
"anatomy",
"skeletal",
"broken",
"xray",
"trauma",
"health",
"vet",
"veterinary",
"crack",
"break",
"damage"
],
"categories": [
"medical",
"animals"
]
}
+18
View File
@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14 4.5a1 1 0 0 1 5 0 .5.5 0 0 0 .5.5 1 1 0 0 1 0 5c-.81 0-1.8-.7-2.5 0l-1.958 1.957a.15.15 0 0 1-.252-.072l-.493-2.07a.15.15 0 0 0-.111-.112l-2.072-.494a.15.15 0 0 1-.072-.252L14 7c.7-.7 0-1.69 0-2.5" />
<path d="m16 20-1-2" />
<path d="m20 16-2-1" />
<path d="m4 8 2 1" />
<path d="m8 4 1 2" />
<path d="M9.698 14.19a.15.15 0 0 0 .112.112l2.074.489a.15.15 0 0 1 .072.252L10 17c-.7.7 0 1.69 0 2.5a1 1 0 0 1-5 0 .495.495 0 0 0-.5-.5 1 1 0 0 1 0-5c.81 0 1.8.7 2.5 0l1.956-1.957a.15.15 0 0 1 .252.072z" />
</svg>

After

Width:  |  Height:  |  Size: 733 B

+1 -2
View File
@@ -29,8 +29,7 @@
{
"name": "book-template",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -8,8 +8,7 @@
{
"name": "curly-braces",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
],
"use-cases": [],
+1 -2
View File
@@ -20,8 +20,7 @@
{
"name": "subtitles",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+12 -1
View File
@@ -3,10 +3,21 @@
"contributors": [
"Andreto",
"mittalyashu",
"ericfennis"
"ericfennis",
"jguddas",
"karsa-mistmere",
"jamiemlaw"
],
"use-cases": [],
"tags": [
"orange",
"healthy",
"nature",
"fresh",
"root",
"produce",
"organic",
"nutrition",
"vegetable",
"food",
"eat"
+3 -3
View File
@@ -9,7 +9,7 @@
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46" />
<path d="M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z" />
<path d="M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z" />
<path d="M15 16a1 1 0 0 0-7-7q-4 4-5.987 12.385a.5.5 0 0 0 .602.602Q11 20 15 16l-3-3" />
<path d="M15 9q4 4 7 0-3-4-7 0 4-4 0-7-4 3 0 7" />
<path d="m8 15-2.58-2.58" />
</svg>

Before

Width:  |  Height:  |  Size: 503 B

After

Width:  |  Height:  |  Size: 383 B

+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "area-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "bar-chart-horizontal-big",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "bar-chart-horizontal",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -33,8 +33,7 @@
{
"name": "candlestick-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "bar-chart-big",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -19,8 +19,7 @@
{
"name": "bar-chart-4",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "bar-chart-3",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -17,8 +17,7 @@
{
"name": "line-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -20,8 +20,7 @@
{
"name": "bar-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -19,8 +19,7 @@
{
"name": "bar-chart-2",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -34,8 +34,7 @@
{
"name": "gantt-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -20,8 +20,7 @@
{
"name": "pie-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "scatter-chart",
"deprecated": true,
"deprecationReason": "alias.name",
"toBeRemovedInVersion": "v1.0"
"deprecationReason": "alias.name"
}
]
}
+1 -2
View File
@@ -18,8 +18,7 @@
{
"name": "alert-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -21,8 +21,7 @@
{
"name": "arrow-down-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -23,8 +23,7 @@
{
"name": "arrow-left-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -17,8 +17,7 @@
{
"name": "arrow-down-left-from-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -17,8 +17,7 @@
{
"name": "arrow-down-right-from-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -21,8 +21,7 @@
{
"name": "arrow-up-left-from-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -17,8 +17,7 @@
{
"name": "arrow-up-right-from-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -23,8 +23,7 @@
{
"name": "arrow-right-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -20,8 +20,7 @@
{
"name": "arrow-up-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}
+1 -2
View File
@@ -21,8 +21,7 @@
{
"name": "check-circle",
"deprecationReason": "alias.name",
"deprecated": true,
"toBeRemovedInVersion": "v1.0"
"deprecated": true
}
]
}

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