diff --git a/.github/workflows/close-issue-with-banned-phrases.yml b/.github/workflows/close-issue-with-banned-phrases.yml
index 2b74a1900..e6958de12 100644
--- a/.github/workflows/close-issue-with-banned-phrases.yml
+++ b/.github/workflows/close-issue-with-banned-phrases.yml
@@ -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})
diff --git a/.github/workflows/repo-journal.yml b/.github/workflows/repo-journal.yml
new file mode 100644
index 000000000..09638bac5
--- /dev/null
+++ b/.github/workflows/repo-journal.yml
@@ -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"
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 4a4204050..86ec55b0b 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -101,6 +101,7 @@ export default defineConfig({
},
{ text: 'Packages', link: '/packages' },
{ text: 'Showcase', link: '/showcase' },
+ { text: 'Merch', link: 'https://merch.lucide.dev/' },
],
sidebar,
socialLinks: [
diff --git a/docs/.vitepress/data/packageData.thirdParty.json b/docs/.vitepress/data/packageData.thirdParty.json
index b430797ab..37f44cad6 100644
--- a/docs/.vitepress/data/packageData.thirdParty.json
+++ b/docs/.vitepress/data/packageData.thirdParty.json
@@ -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"
}
]
diff --git a/docs/.vitepress/getHeadConfig.ts b/docs/.vitepress/getHeadConfig.ts
index 4dacdac2e..95ebe50f0 100644
--- a/docs/.vitepress/getHeadConfig.ts
+++ b/docs/.vitepress/getHeadConfig.ts
@@ -24,13 +24,6 @@ const getHeadConfig = ({
defer: '',
},
],
- [
- 'script',
- {
- src: 'https://media.bitterbrains.com/main.js?from=LUCIDE&type=top',
- async: 'true',
- },
- ],
[
'meta',
{
diff --git a/docs/.vitepress/sidebar/react-native.ts b/docs/.vitepress/sidebar/react-native.ts
index 152a4adc5..7d9a69a5e 100644
--- a/docs/.vitepress/sidebar/react-native.ts
+++ b/docs/.vitepress/sidebar/react-native.ts
@@ -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',
+ },
],
},
{
diff --git a/docs/.vitepress/theme/components/guide/CertificatesLink.vue b/docs/.vitepress/theme/components/guide/CertificatesLink.vue
deleted file mode 100644
index a7d4124c2..000000000
--- a/docs/.vitepress/theme/components/guide/CertificatesLink.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/docs/.vitepress/theme/components/home/HomePackagesSection.vue b/docs/.vitepress/theme/components/home/HomePackagesSection.vue
index b4da0a952..cfc13d320 100644
--- a/docs/.vitepress/theme/components/home/HomePackagesSection.vue
+++ b/docs/.vitepress/theme/components/home/HomePackagesSection.vue
@@ -13,23 +13,21 @@ const { go } = useRouter()
Available For:
-
diff --git a/docs/.vitepress/theme/components/icons/IconPreview.vue b/docs/.vitepress/theme/components/icons/IconPreview.vue
index 5e4b634d6..4cd4f5142 100644
--- a/docs/.vitepress/theme/components/icons/IconPreview.vue
+++ b/docs/.vitepress/theme/components/icons/IconPreview.vue
@@ -3,6 +3,7 @@ import type { IconEntity } from '../../types';
import { computed, ref } from 'vue';
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
import { useIconStyleContext } from '../../composables/useIconStyle';
+import { squarePen } from '../../../data/iconNodes';
const props = defineProps<{
name: IconEntity['name'];
@@ -19,6 +20,31 @@ const iconComponent = computed(() => {
if (!props.name || !props.iconNode) return null;
return createLucideIcon(props.name, props.iconNode);
});
+
+const SquarePenIcon = createLucideIcon('SquarePen', squarePen);
+
+const innerSvg = computed(() => {
+ if (!props.iconNode) return '';
+
+ const string = props.iconNode
+ .map(([tag, attrs]) => {
+ const attributes = Object.entries(attrs)
+ .map(([key, value]) => `${key}="${value}"`)
+ .join(' ');
+ return `<${tag} ${attributes}/>`;
+ })
+ .join('');
+
+ return string
+ .replace(/>[\r\n ]+<')
+ .replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
+ .trim();
+});
+
+const base64InnerSvg = computed(() => {
+ if (typeof window === 'undefined') return '';
+ return btoa(innerSvg.value);
+});
@@ -57,6 +83,16 @@ const iconComponent = computed(() => {
/>
+
+
+
@@ -75,6 +111,8 @@ const iconComponent = computed(() => {
position: relative;
background: var(--vp-c-bg-alt);
border-radius: 8px;
+ overflow: hidden;
+ flex-shrink: 0;
}
.icon-container > :deep(svg:not(.icon-grid)) {
width: 100%;
@@ -85,6 +123,34 @@ const iconComponent = computed(() => {
opacity: 0.8;
}
+.studio-button {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ background: var(--vp-c-bg);
+ border: 1px solid var(--vp-c-divider);
+ border-radius: 6px;
+ padding: 5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--vp-c-text-2);
+ opacity: 0;
+ transition: opacity 0.2s, color 0.2s, background-color 0.2s;
+ z-index: 2;
+}
+
+.icon-container:hover .studio-button,
+.studio-button:focus-visible {
+ opacity: 1;
+}
+
+.studio-button:hover,
+.studio-button:focus-visible {
+ background: var(--vp-c-bg-soft);
+ color: var(--vp-c-text-1);
+}
+
.icon-component.customizable {
will-change: width, height, stroke-width, stroke;
/* color: var(--customize-color, currentColor);
diff --git a/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue b/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
index a2798e497..1fdece4ba 100644
--- a/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
+++ b/docs/.vitepress/theme/components/icons/IconsCategoryOverview.vue
@@ -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(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'));
@@ -178,11 +191,11 @@ function handleCloseDrawer() {
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
- @focus="onFocusSearchInput"
+ @focus="loadSearchMetadata"
/>
(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -110,6 +114,9 @@ const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
]);
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);
@@ -135,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'));
@@ -179,7 +192,7 @@ function handleCloseDrawer() {
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
- @focus="onFocusSearchInput"
+ @focus="loadSearchMetadata"
/>