diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 547a14faa..251223308 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -68,6 +68,12 @@ export default defineConfig({ new URL('./theme/components/overrides/VPCarbonAds.vue', import.meta.url), ), }, + { + find: /^.*\/VPNavBarSearch\.vue$/, + replacement: fileURLToPath( + new URL('./theme/components/overrides/VPNavBarSearch.vue', import.meta.url), + ), + }, { find: '~/.vitepress', replacement: fileURLToPath(new URL('./', import.meta.url)), @@ -125,6 +131,14 @@ export default defineConfig({ editLink: { pattern: 'https://github.com/lucide-icons/lucide/edit/main/docs/:path', }, + search: { + provider: 'algolia', + options: { + appId: 'R7KKMCPXOB', + apiKey: 'dd3101054267d35fb28f49c42715f1aa', + indexName: 'lucide', + }, + }, carbonAds: { code: 'CWYIC53U', placement: 'lucidedev', diff --git a/docs/.vitepress/getStructuredData.ts b/docs/.vitepress/getStructuredData.ts index 2ac47df1e..7fde3ff12 100644 --- a/docs/.vitepress/getStructuredData.ts +++ b/docs/.vitepress/getStructuredData.ts @@ -1,7 +1,19 @@ import { PageData } from 'vitepress'; +/** + * Mirrors the notices in the repo's LICENSE file, which is bumped by hand — keep the + * year here in step with it. + */ +const LUCIDE_COPYRIGHT = 'Copyright (c) 2026 Lucide Icons and Contributors'; +const FEATHER_COPYRIGHT = 'Copyright (c) 2013-present Cole Bemis'; +const LICENSE_URL = 'https://lucide.dev/license'; + export default async function getStructuredData(iconName: string, pageData: PageData) { - const url = `https://lucide.dev/icons/${iconName}`; + const params = pageData.params ?? {}; + const isLab = params.externalLibrary === 'lab'; + const url = `https://lucide.dev/icons/${isLab ? 'lab/' : ''}${iconName}`; + + const awaitingRelease = Boolean(params.awaitingRelease); return { '@context': 'https://schema.org', @@ -29,18 +41,41 @@ export default async function getStructuredData(iconName: string, pageData: Page name: iconName, // TODO: Add support for description extraction from icon metadata // description: 'An ...SVG icon from the Lucide icon set.', - contentUrl: `https://lucide.dev/api/icons/${iconName}}?width=24&height=24&background=white`, - thumbnailUrl: `https://lucide.dev/api/icons/${iconName}}?width=256&height=256&background=white`, + // `/api/icons/` only renders core icons, a lab name would 404 there. + ...(isLab + ? {} + : { + contentUrl: `https://lucide.dev/api/icons/${iconName}?width=24&height=24&background=white`, + thumbnailUrl: `https://lucide.dev/api/icons/${iconName}?width=256&height=256&background=white`, + }), encodingFormat: 'image/svg+xml', - keywords: pageData.params?.tags, + keywords: params.tags, width: 24, height: 24, creator: { '@type': 'Organization', name: 'Lucide Icons', }, - datePublished: pageData?.params?.createdRelease?.date, - dateModified: pageData?.params?.changedRelease?.date, + license: LICENSE_URL, + acquireLicensePage: LICENSE_URL, + // The Feather-derived icons are MIT © Cole Bemis; everything else is ISC. Lab + // icons carry no release metadata, so they fall to the ISC-only notice. + copyrightNotice: params.fromFork + ? `${LUCIDE_COPYRIGHT}; ${FEATHER_COPYRIGHT}` + : LUCIDE_COPYRIGHT, + creditText: 'Lucide Icons', + // Both dates fall back to the date of the fork for unreleased icons, so neither + ...(awaitingRelease + ? {} + : { + ...(params.createdRelease?.date ? { datePublished: params.createdRelease.date } : {}), + ...(params.changedRelease?.date ? { dateModified: params.changedRelease.date } : {}), + }), + + // Custom attributes, picked up by the Algolia crawler to rank and sort the icon + // index. Not schema.org vocabulary, consumers that don't know them ignore them. + popularity: params.popularity ?? 0, + awaitingRelease, }, }; } diff --git a/docs/.vitepress/theme/components/guide/FrameworkSelect.vue b/docs/.vitepress/theme/components/guide/FrameworkSelect.vue index 51e3bd4da..f9171c114 100644 --- a/docs/.vitepress/theme/components/guide/FrameworkSelect.vue +++ b/docs/.vitepress/theme/components/guide/FrameworkSelect.vue @@ -1,10 +1,9 @@ diff --git a/docs/.vitepress/theme/components/overrides/VPNavBarSearch.vue b/docs/.vitepress/theme/components/overrides/VPNavBarSearch.vue new file mode 100644 index 000000000..59305c683 --- /dev/null +++ b/docs/.vitepress/theme/components/overrides/VPNavBarSearch.vue @@ -0,0 +1,206 @@ + + + + + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index 0c9379701..4d11b17aa 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -10,14 +10,14 @@ import HomeHeroInfoBefore from './components/home/HomeHeroInfoBefore.vue'; import { ICON_STYLE_CONTEXT, iconStyleContext } from './composables/useIconStyle'; import { CATEGORY_VIEW_CONTEXT, categoryViewContext } from './composables/useCategoryView'; import { EXTERNAL_LIBS_CONTEXT, externalLibContext } from './composables/useExternalLibs'; -import FrameworkSelect from './components/guide/FrameworkSelect.vue'; +import SideBarNavBefore from './layouts/SideBarNavBefore.vue'; import SnackPlayer from './components/editors/SnackPlayer.vue'; const theme: Partial = { extends: DefaultTheme, Layout() { return h(DefaultTheme.Layout, null, { - 'sidebar-nav-before': () => h(FrameworkSelect), + 'sidebar-nav-before': () => h(SideBarNavBefore), 'home-hero-info-before': () => h(HomeHeroInfoBefore), 'sidebar-nav-after': () => h(IconsSidebarNavAfter), 'home-hero-image': () => h(HomeHeroIconsCard), diff --git a/docs/.vitepress/theme/layouts/SideBarNavBefore.vue b/docs/.vitepress/theme/layouts/SideBarNavBefore.vue new file mode 100644 index 000000000..c0bd505ea --- /dev/null +++ b/docs/.vitepress/theme/layouts/SideBarNavBefore.vue @@ -0,0 +1,20 @@ + + + diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index 234cc4eaf..725715a07 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -129,6 +129,10 @@ html { flex-direction: column-reverse; } +.content-body .search { + display: none; +} + @media (min-width: 960px) { .VPHomeHero .container .image { order: 1; diff --git a/docs/.vitepress/transformPageData.ts b/docs/.vitepress/transformPageData.ts index a57c023d6..85dbc6b61 100644 --- a/docs/.vitepress/transformPageData.ts +++ b/docs/.vitepress/transformPageData.ts @@ -4,11 +4,8 @@ import { createGeneralOGImage, createIconOGImage } from './createOGImage'; export async function transformPageData(pageData: PageData) { pageData.frontmatter.head ??= []; - if ( - pageData.relativePath.startsWith('icons/') && - !pageData.relativePath.startsWith('icons/lab/') && - pageData.params?.name - ) { + if (pageData.relativePath.startsWith('icons/') && pageData.params?.name) { + const isLab = pageData.relativePath.startsWith('icons/lab/'); const iconName = pageData.params.name; pageData.title = `${iconName} icon details`; @@ -24,26 +21,29 @@ export async function transformPageData(pageData: PageData) { const structuredData = await getStructuredData(iconName, pageData); - const ogPath = await createIconOGImage(iconName, pageData.params?.tags || []); + // `createIconOGImage` reads the SVG from `icons/`, where lab icons don't live. + if (!isLab) { + const ogPath = await createIconOGImage(iconName, pageData.params?.tags || []); - if (ogPath) { - const content = `https://lucide.dev${ogPath}`; - pageData.frontmatter.head.push( - [ - 'meta', - { - property: 'og:image', - content, - }, - ], - [ - 'meta', - { - property: 'twitter:image', - content, - }, - ], - ); + if (ogPath) { + const content = `https://lucide.dev${ogPath}`; + pageData.frontmatter.head.push( + [ + 'meta', + { + property: 'og:image', + content, + }, + ], + [ + 'meta', + { + property: 'twitter:image', + content, + }, + ], + ); + } } pageData.frontmatter.head.push([ diff --git a/docs/icons/[name].paths.ts b/docs/icons/[name].paths.ts index 4921fa5e3..75617e7cc 100644 --- a/docs/icons/[name].paths.ts +++ b/docs/icons/[name].paths.ts @@ -3,11 +3,18 @@ import iconNodes from '../.vitepress/data/iconNodes'; import * as iconDetails from '../.vitepress/data/iconDetails'; import { IconEntity } from '../.vitepress/theme/types'; +/** + * The generated `iconDetails` modules import `iconPopularity/.json` as-is, + * so popularity arrives as the raw `{ count }` object instead of a number. + */ +type IconDetail = Omit & { popularity?: { count: number } }; + export default { paths: async () => { - return (Object.values(iconDetails) as unknown as IconEntity[]).map((iconEntity) => { + return (Object.values(iconDetails) as unknown as IconDetail[]).map((iconEntity) => { const params = { ...iconEntity, + popularity: iconEntity.popularity?.count ?? 0, relatedIcons: relatedIcons[iconEntity.name].map((name: string) => ({ name, iconNode: iconNodes[name], diff --git a/docs/package.json b/docs/package.json index 5833b49a8..6ab33a92c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -30,6 +30,7 @@ "author": "Eric Fennis", "license": "ISC", "devDependencies": { + "@docsearch/js": "3.8.2", "@lucide/build-icons": "workspace:*", "@lucide/helpers": "workspace:*", "@lucide/shared": "workspace:*", @@ -57,6 +58,7 @@ "vue-tsc": "^3.3.11" }, "dependencies": { + "@docsearch/css": "3.8.2", "@floating-ui/vue": "^2.0.1", "@headlessui/vue": "^1.7.23", "@lucide/vue": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 983fb2ed9..ba0a83d36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: docs: dependencies: + '@docsearch/css': + specifier: 3.8.2 + version: 3.8.2 '@floating-ui/vue': specifier: ^2.0.1 version: 2.0.1(vue@3.5.43(typescript@6.0.3)) @@ -186,6 +189,9 @@ importers: specifier: ^3.5.43 version: 3.5.43(typescript@6.0.3) devDependencies: + '@docsearch/js': + specifier: 3.8.2 + version: 3.8.2(@algolia/client-search@5.57.0)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(search-insights@2.8.2) '@lucide/build-icons': specifier: workspace:* version: link:../tools/build-icons @@ -1273,19 +1279,19 @@ importers: version: 6.1.0(@babel/core@7.29.7(supports-color@10.2.2))(@types/babel__core@7.20.5)(rollup@4.63.1)(supports-color@10.2.2) '@solidjs/testing-library': specifier: next - version: 1.0.0-beta.3(@solidjs/web@2.0.0-rc.14(solid-js@2.0.0-rc.14))(solid-js@2.0.0-rc.14) + version: 1.0.0-beta.3(@solidjs/web@2.0.0-rc.15(solid-js@2.0.0-rc.15))(solid-js@2.0.0-rc.15) '@solidjs/vite-plugin': specifier: next - version: 3.0.0-next.35(@solidjs/web@2.0.0-rc.14(solid-js@2.0.0-rc.14))(@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3))(solid-js@2.0.0-rc.14)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0)) + version: 3.0.0-next.35(@solidjs/web@2.0.0-rc.15(solid-js@2.0.0-rc.15))(@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3))(solid-js@2.0.0-rc.15)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0)) '@solidjs/web': specifier: next - version: 2.0.0-rc.14(solid-js@2.0.0-rc.14) + version: 2.0.0-rc.15(solid-js@2.0.0-rc.15) '@testing-library/jest-dom': specifier: ^7.0.1 version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3) babel-preset-solid: specifier: next - version: 2.0.0-rc.2(@babel/core@7.29.7(supports-color@10.2.2))(solid-js@2.0.0-rc.14) + version: 2.0.0-rc.2(@babel/core@7.29.7(supports-color@10.2.2))(solid-js@2.0.0-rc.15) esbuild: specifier: ^0.28.2 version: 0.28.2 @@ -1300,7 +1306,7 @@ importers: version: 6.5.1(rollup@4.63.1)(typescript@6.0.3) solid-js: specifier: next - version: 2.0.0-rc.14 + version: 2.0.0-rc.15 typescript: specifier: ^6.0.3 version: 6.0.3 @@ -5710,8 +5716,8 @@ packages: resolution: {integrity: sha512-4+pRdrAHtfyE9BUJWBup3TpzJojJjgW2mV1vm/Jik4tWa5epxXB/YrLkwqP1v8+S9XjyKKZu5BSLqmcpswMYeQ==} engines: {node: '>=22.12.0'} - '@solidjs/signals@2.0.0-rc.14': - resolution: {integrity: sha512-s3v1wpPcX3wfNO0WUERrPrXS+qqJ4ylzH9QTIRKqrgcMVeLEOXgFa3hZrhEoVTp+lVAgido2NH6GCI79twe2fg==} + '@solidjs/signals@2.0.0-rc.15': + resolution: {integrity: sha512-rMhLaYnBvpb9sB3NJGPsqIxjgr0XKU2nvBS7jhWX2fKx+K/m5O0VoAuCgYuTie3BMBZU9vX08NdnFXg508pcew==} engines: {node: '>=22.12.0'} '@solidjs/start@2.0.5': @@ -5777,11 +5783,11 @@ packages: peerDependencies: solid-js: ^2.0.0-rc.13 - '@solidjs/web@2.0.0-rc.14': - resolution: {integrity: sha512-MhHqUoVBdgAG7EZOANx7Y5SuCFH7sBxzt9uBSYLpJxE503tqoi7pX1UhV7PWxw4LvmPe1PZkdkSOZ8BLchZkhQ==} + '@solidjs/web@2.0.0-rc.15': + resolution: {integrity: sha512-xwt4rNZ9CXTdMraFqmVn/AXuHIXQJ3jnrKyzEZ4ZFdRCBml0VxL8pA5D3YZHSjHoF7okNAmiVtyRCMNz15jQCg==} engines: {node: '>=22.12.0'} peerDependencies: - solid-js: ^2.0.0-rc.14 + solid-js: ^2.0.0-rc.15 '@speed-highlight/core@1.2.24': resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} @@ -12164,8 +12170,8 @@ packages: resolution: {integrity: sha512-62bYOI4JZ15KOqL5eReKyWSwAXrGb0fbX8SDnHsbJk2UX+SzSyy1gobas2cxW/0BXcobGfQ1V6ffPyQkMIBdoQ==} engines: {node: '>=22.12.0'} - solid-js@2.0.0-rc.14: - resolution: {integrity: sha512-fmaqUHgbITf18Hedur6gPnaPE4GrdPgkTbLujHqM5Vl5jxWf9rxwFh6hZMXQs+Kg1pu8KYuOMfqnuSizFQ9I6Q==} + solid-js@2.0.0-rc.15: + resolution: {integrity: sha512-HXiWAEG5Rjp5C7reElUlvdriIadcbpwWwVdztTf0wuxeirw49rGuIzVbSRn06p6GjlINwi5d24Z46b478hNdvw==} engines: {node: '>=22.12.0'} solid-refresh@0.6.3: @@ -18200,7 +18206,7 @@ snapshots: '@solidjs/signals@2.0.0-rc.13': {} - '@solidjs/signals@2.0.0-rc.14': {} + '@solidjs/signals@2.0.0-rc.15': {} '@solidjs/start@2.0.5(@solidjs/router@1.0.0(solid-js@1.9.15))(@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3))(crossws@0.4.12(srvx@1.0.5))(ocache@0.3.0)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0))': dependencies: @@ -18251,22 +18257,22 @@ snapshots: '@testing-library/dom': 10.4.1 solid-js: 2.0.0-rc.13 - '@solidjs/testing-library@1.0.0-beta.3(@solidjs/web@2.0.0-rc.14(solid-js@2.0.0-rc.14))(solid-js@2.0.0-rc.14)': + '@solidjs/testing-library@1.0.0-beta.3(@solidjs/web@2.0.0-rc.15(solid-js@2.0.0-rc.15))(solid-js@2.0.0-rc.15)': dependencies: - '@solidjs/web': 2.0.0-rc.14(solid-js@2.0.0-rc.14) + '@solidjs/web': 2.0.0-rc.15(solid-js@2.0.0-rc.15) '@testing-library/dom': 10.4.1 - solid-js: 2.0.0-rc.14 + solid-js: 2.0.0-rc.15 - '@solidjs/vite-plugin@3.0.0-next.35(@solidjs/web@2.0.0-rc.14(solid-js@2.0.0-rc.14))(@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3))(solid-js@2.0.0-rc.14)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0))': + '@solidjs/vite-plugin@3.0.0-next.35(@solidjs/web@2.0.0-rc.15(solid-js@2.0.0-rc.15))(@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.3))(solid-js@2.0.0-rc.15)(supports-color@10.2.2)(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@babel/core': 7.29.7(supports-color@10.2.2) '@solidjs/babel-plugin': 2.0.0-rc.13(@babel/core@7.29.7(supports-color@10.2.2)) '@solidjs/compiler': 2.0.0-rc.13 - '@solidjs/web': 2.0.0-rc.14(solid-js@2.0.0-rc.14) + '@solidjs/web': 2.0.0-rc.15(solid-js@2.0.0-rc.15) '@types/babel__core': 7.20.5 merge-anything: 5.1.7 - solid-js: 2.0.0-rc.14 + solid-js: 2.0.0-rc.15 vite: 8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0) vitefu: 1.1.3(vite@8.3.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(sass-embedded@1.105.1)(sass@1.105.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0)) optionalDependencies: @@ -18299,11 +18305,11 @@ snapshots: seroval-plugins: 1.6.7(seroval@1.6.7) solid-js: 2.0.0-rc.13 - '@solidjs/web@2.0.0-rc.14(solid-js@2.0.0-rc.14)': + '@solidjs/web@2.0.0-rc.15(solid-js@2.0.0-rc.15)': dependencies: seroval: 1.6.7 seroval-plugins: 1.6.7(seroval@1.6.7) - solid-js: 2.0.0-rc.14 + solid-js: 2.0.0-rc.15 '@speed-highlight/core@1.2.24': {} @@ -20167,12 +20173,12 @@ snapshots: optionalDependencies: solid-js: 1.9.15 - babel-preset-solid@2.0.0-rc.2(@babel/core@7.29.7(supports-color@10.2.2))(solid-js@2.0.0-rc.14): + babel-preset-solid@2.0.0-rc.2(@babel/core@7.29.7(supports-color@10.2.2))(solid-js@2.0.0-rc.15): dependencies: '@babel/core': 7.29.7(supports-color@10.2.2) '@dom-expressions/babel-plugin-jsx': 0.50.0-next.44(@babel/core@7.29.7(supports-color@10.2.2)) optionalDependencies: - solid-js: 2.0.0-rc.14 + solid-js: 2.0.0-rc.15 bail@2.0.2: {} @@ -25938,9 +25944,9 @@ snapshots: seroval: 1.6.7 seroval-plugins: 1.6.7(seroval@1.6.7) - solid-js@2.0.0-rc.14: + solid-js@2.0.0-rc.15: dependencies: - '@solidjs/signals': 2.0.0-rc.14 + '@solidjs/signals': 2.0.0-rc.15 csstype: 3.2.3 seroval: 1.6.7 seroval-plugins: 1.6.7(seroval@1.6.7)