Adjust search with algolia

This commit is contained in:
Eric Fennis
2026-09-21 08:54:55 +02:00
parent 951813ce76
commit 420e22b5f1
14 changed files with 396 additions and 316 deletions
+14
View File
@@ -0,0 +1,14 @@
# Copy to `docs/.env`. Every variable is optional.
# Icon popularity from Plausible. Defaults to 0 for every icon when unset.
LUCIDE_ANALYTICS_TOKEN=
# Set to `true` to also show icons that are not released yet.
VITE_SHOW_AWAITING_RELEASE=
# Client-side Algolia icon search (public search-only key).
# Overrides the defaults in docs/.vitepress/theme/utils/algolia.ts,
# e.g. to point the site at another index. The index is filled by the Algolia crawler.
VITE_ALGOLIA_APP_ID=
VITE_ALGOLIA_SEARCH_API_KEY=
VITE_ALGOLIA_INDEX_NAME=
+8
View File
@@ -118,6 +118,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: 'docs'
}
},
carbonAds: {
code: 'CWYIC53U',
placement: 'lucidedev',
@@ -1,20 +1,19 @@
<script setup lang="ts">
import { ref, computed, watch, defineAsyncComponent, onMounted } from 'vue';
import type { IconEntity, Category } from '../../types';
import useSearch from '../../composables/useSearch';
import useIconSearch, { entityId } from '../../composables/useIconSearch';
import InputSearch from '../base/InputSearch.vue';
import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
import StickyBar from './StickyBar.vue';
import IconsCategory, { CategoryRow } 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 useScrollToCategory from '../../composables/useScrollToCategory';
import { useCategoryView } from '../../composables/useCategoryView';
import CarbonAdOverlay from './CarbonAdOverlay.vue';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
import { prefetchAlgoliaClient } from '../../utils/algolia';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
@@ -28,7 +27,7 @@ const props = defineProps<{
const activeIconName = ref(null);
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
const { selectedCategory } = useCategoryView();
const isSearching = computed(() => !!searchQuery.value);
const isSearching = computed(() => !!searchQueryDebounced.value);
watch(searchQueryDebounced, (searchString) => {
if (searchString !== '') {
@@ -44,13 +43,6 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
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);
@@ -58,27 +50,14 @@ const columnSize = computed(() => {
return Math.floor(containerWidth.value / (ICON_SIZE + ICON_GRID_GAP));
});
const mappedIcons = computed(() => {
if (tags.value == null) {
return props.icons;
}
return props.icons.map((icon) => {
const iconTags = tags.value[icon.name];
const iconCategories = categoriesMap.value?.[icon.name] ?? [];
const icons = computed(() => props.icons);
return {
...icon,
tags: iconTags,
categories: iconCategories,
};
});
});
const { results: searchResults, isPending: isSearchPending } = useIconSearch(
searchQueryDebounced,
icons,
);
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 3 },
{ name: 'tags', weight: 2 },
]);
const resultIds = computed(() => new Set(searchResults.value.map(entityId)));
const categories = computed(() => {
if (!props.categories?.length || !props.icons?.length) return [];
@@ -92,10 +71,8 @@ const categories = computed(() => {
return iconCategories?.includes(name);
});
const searchedCategoryIcons = isSearching
? categoryIcons.filter((icon) =>
searchResults.value.some((item) => item?.name === icon?.name),
)
const searchedCategoryIcons = isSearching.value
? categoryIcons.filter((icon) => resultIds.value.has(entityId(icon)))
: categoryIcons;
return {
@@ -121,8 +98,8 @@ const categoriesList = computed(() => {
}, []);
});
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && !isFetchingTags && !isFetchingCategories,
const isSearchSettling = computed(
() => searchQuery.value !== searchQueryDebounced.value || isSearchPending.value,
);
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
@@ -142,21 +119,6 @@ onMounted(() => {
useEventListener(window, 'scroll', containerProps.onScroll);
});
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
}
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'));
@@ -191,11 +153,11 @@ function handleCloseDrawer() {
:shortcut="kbdSearchShortcut"
class="input-wrapper"
ref="searchInput"
@focus="loadSearchMetadata"
@focus="prefetchAlgoliaClient"
/>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
v-if="searchPlaceholder.isNoResults && !isSearchSettling"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"
@@ -6,21 +6,21 @@ 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 useIconSearch from '../../composables/useIconSearch';
import useSearchInput from '../../composables/useSearchInput';
import useSearchShortcut from '../../utils/useSearchShortcut';
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';
import useSearchPlaceholder from '../../utils/useSearchPlaceholder.ts';
import { sortIcons, type SortKey } from '../../utils/sortIcons';
import { prefetchAlgoliaClient } from '../../utils/algolia';
import Icon from '@lucide/vue/src/Icon';
import { listSortDescending } from '~/.vitepress/data/iconNodes';
const ICON_SIZE = 56;
const ICON_GRID_GAP = 8;
const SORTING = [
const SORTING: { name: string; value: SortKey }[] = [
{
name: 'Popularity',
value: 'popularity',
@@ -33,7 +33,7 @@ const SORTING = [
name: 'Name',
value: 'name',
},
]
];
const initialGridItems = computed(() => {
if (containerWidth.value === 0) return 120;
@@ -49,14 +49,7 @@ const props = defineProps<{
}>();
const activeIconName = ref(null);
const selectedSort = ref(SORTING[0])
const { execute: fetchTags, data: tags, isFetching: isFetchingTags } = useFetchTags();
const {
execute: fetchCategories,
data: categories,
isFetching: isFetchingCategories,
} = useFetchCategories();
const selectedSort = ref(SORTING[0]);
const overviewEl = ref<HTMLElement | null>(null);
const { width: containerWidth } = useElementSize(overviewEl);
@@ -65,45 +58,7 @@ 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) => {
if (a.awaitingRelease !== b.awaitingRelease) return a.awaitingRelease ? -1 : 1;
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 sortedIcons.value;
}
if (categories.value == null) {
return sortedIcons.value;
}
return sortedIcons.value.map((icon) => {
const iconTags = tags.value[icon.name];
const iconCategories = categories.value[icon.name] ?? [];
return {
...icon,
tags: iconTags,
categories: iconCategories,
};
});
});
const sortedIcons = computed(() => sortIcons(props.icons, selectedSort.value.value));
const { searchInput, searchQuery, searchQueryDebounced } = useSearchInput();
@@ -111,20 +66,28 @@ const { shortcutText: kbdSearchShortcut } = useSearchShortcut(() => {
searchInput.value?.focus();
});
const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
{ name: 'name', weight: 3 },
{ name: 'aliases', weight: 8 },
{ name: 'tags', weight: 2 },
{ name: 'categories', weight: 1 },
]);
const { results: searchResults, isPending: isSearchPending } = useIconSearch(
searchQueryDebounced,
sortedIcons,
);
// While searching, "Popularity" keeps Algolia's relevance order.
// The other sort options re-sort the matching icons locally.
const displayedIcons = computed(() => {
if (searchQueryDebounced.value && selectedSort.value.value !== 'popularity') {
return sortIcons(searchResults.value, selectedSort.value.value);
}
return searchResults.value;
});
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
const isSearchMetadataLoading = computed(
() => searchQuery.value.length > 0 && !isFetchingTags && !isFetchingCategories,
const isSearchSettling = computed(
() => searchQuery.value !== searchQueryDebounced.value || isSearchPending.value,
);
const chunkedIcons = computed(() => {
return chunkArray(searchResults.value, columnSize.value);
return chunkArray(displayedIcons.value, columnSize.value);
});
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(chunkedIcons, {
@@ -147,21 +110,6 @@ function setActiveIconName(name: string) {
activeIconName.value = name;
}
function loadSearchMetadata() {
if (tags.value == null && !isFetchingTags.value) {
void fetchTags();
}
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'));
@@ -197,7 +145,7 @@ function handleCloseDrawer() {
ref="searchInput"
:shortcut="kbdSearchShortcut"
class="input-wrapper"
@focus="loadSearchMetadata"
@focus="prefetchAlgoliaClient"
/>
<Select
@@ -216,7 +164,7 @@ function handleCloseDrawer() {
</Select>
</StickyBar>
<NoResults
v-if="searchPlaceholder.isNoResults && !isSearchMetadataLoading"
v-if="searchPlaceholder.isNoResults && !isSearchSettling"
:searchQuery="searchPlaceholder.query"
:isBrandSearch="searchPlaceholder.isBrand"
@clear="searchQuery = ''"
@@ -224,7 +172,7 @@ function handleCloseDrawer() {
<IconGrid
v-else-if="list.length === 0"
overlayMode
:icons="searchResults.slice(0, initialGridItems)"
:icons="displayedIcons.slice(0, initialGridItems)"
:activeIcon="activeIconName"
@setActiveIcon="setActiveIconName"
/>
@@ -1,12 +0,0 @@
import { useFetch } from '@vueuse/core';
const useFetchCategories = () =>
useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/categories`,
{
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('search'),
},
).json();
export default useFetchCategories;
@@ -1,12 +0,0 @@
import { useFetch } from '@vueuse/core';
const useFetchTags = () =>
useFetch<Record<string, string[]>>(
`${import.meta.env.DEV ? 'http://localhost:3000' : ''}/api/tags`,
{
immediate:
typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('search'),
},
).json();
export default useFetchTags;
@@ -0,0 +1,81 @@
import { computed, ref, shallowRef, watch, type ComputedRef, type Ref } from 'vue';
import { searchIconIds } from '../utils/algolia';
export interface SearchableIcon {
name: string;
externalLibrary?: string;
}
/**
* Matches the `objectID` scheme of the Algolia `icons` index:
* `name` for core icons, `lab/<name>` for lab icons.
*/
export const entityId = (icon: SearchableIcon) =>
icon.externalLibrary ? `${icon.externalLibrary}/${icon.name}` : icon.name;
export interface UseIconSearchResult<T> {
/** Matching icons in relevance order, or the full collection when the query is empty. */
results: ComputedRef<T[]>;
/** `true` while a search request is in flight. */
isPending: Ref<boolean>;
}
const useIconSearch = <T extends SearchableIcon>(
query: Ref<string>,
collection: Ref<T[]>,
): UseIconSearchResult<T> => {
const byId = computed(() => new Map(collection.value.map((icon) => [entityId(icon), icon])));
const hitIds = shallowRef<string[]>([]);
const isPending = ref(false);
let requestSeq = 0;
watch(
query,
async (searchQuery) => {
const seq = ++requestSeq;
if (!searchQuery) {
isPending.value = false;
return;
}
isPending.value = true;
try {
const ids = await searchIconIds(searchQuery);
if (seq === requestSeq) {
hitIds.value = ids;
}
} catch (error) {
console.error('Icon search failed', error);
if (seq === requestSeq) {
hitIds.value = [];
}
} finally {
if (seq === requestSeq) {
isPending.value = false;
}
}
},
{ immediate: true },
);
const results = computed(() => {
if (!query.value) {
return collection.value;
}
// Hits without a local entity are dropped: lab icons while the Lab library is not
// loaded, or icons the crawler indexed after this build was deployed.
return hitIds.value
.map((id) => byId.value.get(id))
.filter((icon): icon is T => icon !== undefined);
});
return { results, isPending };
};
export default useIconSearch;
@@ -1,32 +0,0 @@
import Fuse, { type FuseOptionKeyObject } from 'fuse.js';
import { shallowRef, computed, Ref } from 'vue';
const useSearch = <T>(
query: Ref<string>,
collection: Ref<T[]>,
keys: FuseOptionKeyObject<T>[] = [],
) => {
const index = shallowRef(
new Fuse(collection.value, {
threshold: 0.2,
useExtendedSearch: true,
keys,
}),
);
const results = computed(() => {
index.value.setCollection(collection.value);
if (query.value) {
return index.value
.search({ $and: query.value.split(' ').filter((t) => !!t) })
.map((result) => result.item);
}
return collection.value;
});
return results;
};
export default useSearch;
+83
View File
@@ -0,0 +1,83 @@
import type { LiteClient } from 'algoliasearch/lite';
/**
* Public search credentials. The search-only key is meant to be shipped in the client
* bundle, so local dev, forks and preview deployments search the production index
* without any configuration. `VITE_ALGOLIA_*` env vars override these defaults.
*
* The index itself is populated by the Algolia crawler; nothing in the docs writes to it.
*/
const DEFAULTS = {
appId: 'R7KKMCPXOB',
searchApiKey: 'dd3101054267d35fb28f49c42715f1aa',
indexName: 'icons',
};
export const ALGOLIA_APP_ID = import.meta.env.VITE_ALGOLIA_APP_ID || DEFAULTS.appId;
export const ALGOLIA_SEARCH_API_KEY =
import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY || DEFAULTS.searchApiKey;
export const ALGOLIA_INDEX_NAME = import.meta.env.VITE_ALGOLIA_INDEX_NAME || DEFAULTS.indexName;
/**
* The index caps retrievable hits at 1000 per query (`paginationLimitedTo`),
* so a single page of this size is the most we can ever get back.
*/
const HITS_PER_PAGE = 1000;
let clientPromise: Promise<LiteClient> | undefined;
/**
* Lazily loads the Algolia lite client so it stays out of the initial page bundle.
* Call it on search input focus to prefetch before the first keystroke.
*/
export function getAlgoliaClient(): Promise<LiteClient> {
clientPromise ??= import('algoliasearch/lite')
.then(({ liteClient }) => liteClient(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY))
.catch((error) => {
// Allow a retry on the next call instead of caching the failure.
clientPromise = undefined;
throw error;
});
return clientPromise;
}
export function prefetchAlgoliaClient() {
getAlgoliaClient().catch(() => {
// Ignored here, the actual search reports errors.
});
}
const cache = new Map<string, string[]>();
/**
* Searches the icon index and returns the matching `objectID`s in relevance order.
* Core icons use their name as `objectID`, lab icons use `lab/<name>`.
*/
export async function searchIconIds(query: string): Promise<string[]> {
const cached = cache.get(query);
if (cached) {
return cached;
}
const client = await getAlgoliaClient();
const {
results: [result],
} = await client.searchForHits<{ objectID: string }>({
requests: [
{
indexName: ALGOLIA_INDEX_NAME,
query,
hitsPerPage: HITS_PER_PAGE,
attributesToRetrieve: ['objectID'],
attributesToHighlight: [],
},
],
});
const ids = result.hits.map((hit) => hit.objectID);
cache.set(query, ids);
return ids;
}
+24
View File
@@ -0,0 +1,24 @@
import type { IconEntity } from '../types';
export type SortKey = 'popularity' | 'release-date' | 'name';
type SortableIcon = Pick<IconEntity, 'name' | 'popularity' | 'createdRelease' | 'awaitingRelease'>;
export function sortIcons<T extends SortableIcon>(icons: T[], sort: SortKey): T[] {
switch (sort) {
case 'popularity':
return [...icons].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
case 'release-date':
return [...icons].sort((a, b) => {
if (a.awaitingRelease !== b.awaitingRelease) return a.awaitingRelease ? -1 : 1;
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 [...icons].sort((a, b) => a.name.localeCompare(b.name));
default:
return icons;
}
}
+7
View File
@@ -35,6 +35,13 @@ declare global {
// Defined by VitePress at build time, used by its VPDocAsideCarbonAds component
const __CARBON__: boolean;
// Optional overrides for the Algolia icon search, see docs/.env.example
interface ImportMetaEnv {
readonly VITE_ALGOLIA_APP_ID?: string;
readonly VITE_ALGOLIA_SEARCH_API_KEY?: string;
readonly VITE_ALGOLIA_INDEX_NAME?: string;
}
interface Window {
_carbonads?: unknown;
ExpoSnack?: {
+8
View File
@@ -18,6 +18,14 @@ pnpm docs:dev
pnpm dev
```
## Environment variables
Copy `.env.example` to `.env` if you need any of them. All variables are optional; see the comments in `.env.example`.
## Icon search
Search on `/icons` and `/icons/categories` queries the Algolia `icons` index, which is filled by the Algolia crawler. The site only reads from it (`.vitepress/theme/utils/algolia.ts`), it never writes to it.
## Build
```sh
+1 -1
View File
@@ -61,8 +61,8 @@
"@vueuse/components": "^14.4.0",
"@vueuse/core": "^14.4.0",
"@vueuse/integrations": "^14.4.0",
"algoliasearch": "^5.59.0",
"element-to-path": "^1.2.1",
"fuse.js": "^7.5.0",
"jszip": "^3.10.1",
"lodash-es": "^4.18.1",
"lucide": "workspace:*",
+128 -127
View File
@@ -149,12 +149,12 @@ importers:
'@vueuse/integrations':
specifier: ^14.4.0
version: 14.4.0(focus-trap@7.8.0)(fuse.js@7.5.0)(vue@3.5.42(typescript@6.0.3))
algoliasearch:
specifier: ^5.59.0
version: 5.59.0
element-to-path:
specifier: ^1.2.1
version: 1.2.1
fuse.js:
specifier: ^7.5.0
version: 7.5.0
jszip:
specifier: ^3.10.1
version: 3.10.1
@@ -242,7 +242,7 @@ importers:
version: 6.0.3
vitepress:
specifier: ^1.6.4
version: 1.6.4(@algolia/client-search@5.57.0)(@types/node@26.4.1)(@types/react@18.3.31)(fuse.js@7.5.0)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(lightningcss@1.33.0)(postcss@8.5.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.103.1)(search-insights@2.8.2)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(typescript@6.0.3)(yaml@2.9.0)
version: 1.6.4(@algolia/client-search@5.59.0)(@types/node@26.4.1)(@types/react@18.3.31)(fuse.js@7.5.0)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(lightningcss@1.33.0)(postcss@8.5.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.103.1)(search-insights@2.8.2)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(typescript@6.0.3)(yaml@2.9.0)
vitepress-plugin-group-icons:
specifier: ^1.7.6
version: 1.7.6(vite@7.3.6(@types/node@26.4.1)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(lightningcss@1.33.0)(sass@1.103.1)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(yaml@2.9.0))
@@ -1119,8 +1119,8 @@ packages:
'@adobe/css-tools@4.5.0':
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
'@algolia/abtesting@1.23.0':
resolution: {integrity: sha512-j45MBISstltys9QyQ4xf6quRiN1g7vMuwQL9VM4dx8YuRZvCQ173b9royZAx6iAbRX3IB1VnG1z//NuwyQ8jpQ==}
'@algolia/abtesting@1.25.0':
resolution: {integrity: sha512-rSTin9Uta23uaewYVQEp8XI9T3iA/zrg0/1G2vhf8oFFDxFL5vybnZ5IQwsVAg4JpKxPX4/WYNKdcfWrZymk7w==}
engines: {node: '>= 14.0.0'}
'@algolia/autocomplete-core@1.17.7':
@@ -1143,56 +1143,56 @@ packages:
'@algolia/client-search': '>= 4.9.1 < 6'
algoliasearch: '>= 4.9.1 < 6'
'@algolia/client-abtesting@5.57.0':
resolution: {integrity: sha512-JVFFujiZUCguk5tz3LZr4fTQxqpIrj4/Jw3SI7kMljSqtfLxYn/s/TWH0J2s4iNfsDpxPhgFGMotCpmDI4kZ8w==}
'@algolia/client-abtesting@5.59.0':
resolution: {integrity: sha512-bm2XN0hCSMYwStSsCBT0/PUB2BDxoyR1Lnub3c392HMEy9bi8PUSW8vR6zltVEKWG2t4PQFWMD5C07bmJxGgPg==}
engines: {node: '>= 14.0.0'}
'@algolia/client-analytics@5.57.0':
resolution: {integrity: sha512-6KqECK4ED3JJQEoDrQWnGPQzElA828xAD4qK5ceawNNyP/LcSvzAoLHjFkoTPksZ/kxj6VUtCRH+IHZesLltng==}
'@algolia/client-analytics@5.59.0':
resolution: {integrity: sha512-XOFPOTa69WuqHR6c5tMgnUUwwqQgNSzMpxmhrgA9KmxRf8WIqEa0cokHJvohk5CYb7CZx0xeSL6Bk2IUJ7Lv7Q==}
engines: {node: '>= 14.0.0'}
'@algolia/client-common@5.57.0':
resolution: {integrity: sha512-uqpGF3oXYsoCbQq5d7BzNrNTfIfuvJyGP1CKvSW27T9boUg7KOwyxsAw1AX0a3jSW2HrYEJ/NN+Z4MiGivbpeQ==}
'@algolia/client-common@5.59.0':
resolution: {integrity: sha512-PC8ipLOYFKRTfIUY1J3FJxS6ryzWziaXmIX9/sNMoUR8L+XhF7hX2QAeUI87YVl5zujvlYTSOb+HAIqKXDjyHQ==}
engines: {node: '>= 14.0.0'}
'@algolia/client-insights@5.57.0':
resolution: {integrity: sha512-u5NboJVJXDEFplvNnqqX4CxkXPYysjJRj47hOSh9329H8kG5gFLKJBIiS5utMQ+GZm8xQl3Te7NInDk6elEADQ==}
'@algolia/client-insights@5.59.0':
resolution: {integrity: sha512-yFNcCMM5fHiyoR0HuxMrzy+VjDcmhFUZTm2IJ2DHwGsVH3B5SEob4zTmeEZ3j/AZqNnmrJOCKl/tJnBg7+84bA==}
engines: {node: '>= 14.0.0'}
'@algolia/client-personalization@5.57.0':
resolution: {integrity: sha512-uzc0b2LmHAK9/QID4xeo35OG84AkZl4YewkCqawqAOGLjT2eZpM/OZx45ESygMHG30Ws+ZTSdluPtMJcUnrbWQ==}
'@algolia/client-personalization@5.59.0':
resolution: {integrity: sha512-GYja6HkDt2VrQhWmB2cLx3Z5fDwI9no7q+xwCWcFrTPm5CLH9QZvK0XLO9co1FcNIDxxkMaP3AXM39/XcecEOg==}
engines: {node: '>= 14.0.0'}
'@algolia/client-query-suggestions@5.57.0':
resolution: {integrity: sha512-dIAhnM6ue/ssa5PjgNfu4g8A4yTojl9ZOUzZU3wIaIKRerL2R/3Emuf9n/D6ICXXP167KC6XCeC7nliSw7cuSw==}
'@algolia/client-query-suggestions@5.59.0':
resolution: {integrity: sha512-Wofg7bMpWh8N5qDDZs0wy6whc+KMmdNsxgrIGp9Ug2s1Bka0uq7AjyckdxReKPHLA0Q1qH8X/SNh0wn2t2X2Lw==}
engines: {node: '>= 14.0.0'}
'@algolia/client-search@5.57.0':
resolution: {integrity: sha512-2TTPTTKSJmCptvhCm4Xf3bBYMqZni+Pgc2hVdqc4l9wsBpSJNVTVIKpnd10OubUgkGcmppVDj1XQqYaf6EnPSQ==}
'@algolia/client-search@5.59.0':
resolution: {integrity: sha512-fHnALZfbEnODczGk14Y/1YBRApp6UEpZUTexGcMUPzY7RDc7q4HN2Y6jh0KUG8Jo9B+q+wOoQEc3ziR0ErbuFg==}
engines: {node: '>= 14.0.0'}
'@algolia/ingestion@1.57.0':
resolution: {integrity: sha512-W4JseHKt+pzOxlFV+T3MWEG0h4Z2Se5zjoXUD0ewlw8aOWMG/yjRdopUdLQsXULepB/My2tDuZjkwk2sMfsUrQ==}
'@algolia/ingestion@1.59.0':
resolution: {integrity: sha512-Fa38s1mHgoaLCT117sfJ6P78rtxUt93CYxBYpq1VOIcslaF+cH+1h5uikAPsKfxLnsuEDZHiBiaI6eNPTsQMRA==}
engines: {node: '>= 14.0.0'}
'@algolia/monitoring@1.57.0':
resolution: {integrity: sha512-BrxJVE0/eLinEPICCD7BKN/2xnt0nkjge70u8zzE2ISP3fuB3tjLgcwpanUycvlHBFLI4gK0l5ol54p6IYuR/Q==}
'@algolia/monitoring@1.59.0':
resolution: {integrity: sha512-NyNsRSqM2tF1MX7ZGw/j4rduoEJiQ5wfvbSo/CFVdEzYCjyxbGFRPOZyS/GETJG9rk1TdHOq8NZqKqk/u1fm4g==}
engines: {node: '>= 14.0.0'}
'@algolia/recommend@5.57.0':
resolution: {integrity: sha512-Gc29jkeiLKlVfHvyrIgyUHHE+aYTdXEeLfK42rjr5/1TTVsYwUJz0XkvoIBIqfMjcDg6gXeHb1jTUZ0H+SYIlQ==}
'@algolia/recommend@5.59.0':
resolution: {integrity: sha512-nXBK2uygWtvbCOffMWqqfjjcEtp9enDKY5/2Pw/Hhw8VVaOyK2ThbGK04bNX/Le+qoK1VHYkodWGVTKfw0Otkw==}
engines: {node: '>= 14.0.0'}
'@algolia/requester-browser-xhr@5.57.0':
resolution: {integrity: sha512-PIPnPN7MP3fp2VAi01BVXhCWmD366ZB2Hkq5TlYKtThd4KxUtMmaaNDpFgVCTXtSIqWVZLJntOHRvxg/sIPd8Q==}
'@algolia/requester-browser-xhr@5.59.0':
resolution: {integrity: sha512-yb+4afX/zja8QwX0KmV4/ae2kyYkRknsE79CRusYS2U5D8qwn2wq0cn1x12f3tm3F3+5FgmDdbuGdQTuCLSKMQ==}
engines: {node: '>= 14.0.0'}
'@algolia/requester-fetch@5.57.0':
resolution: {integrity: sha512-AX3RlOudXMdTwtwUqdAf5hAVLvXfOZZH1FZh6ALDdrhVLT0TtAIe48N6nYcWcTnwoTxK/wDIQqZ19IMjK8zJAA==}
'@algolia/requester-fetch@5.59.0':
resolution: {integrity: sha512-Lp52TmpA1QtNmdHzs505Xwf4tgII7RAapkDSF9AAtWIBETcGaTIaXci4Rd9nS6SdaWK1BWTSqAPp5wzh6UN3Ag==}
engines: {node: '>= 14.0.0'}
'@algolia/requester-node-http@5.57.0':
resolution: {integrity: sha512-cWZc1dKb7wy9/wPpwMtL1y89gK2G7y2A47Coa7zwf1ydtIeJm4+S+XxoQ2b/ZRiQnrC1YHavLjYUPDCdnT7Khg==}
'@algolia/requester-node-http@5.59.0':
resolution: {integrity: sha512-YYHLEs5rC6oRTFwT7bJaKBR9NSFzikeVHGAT1ffATmUQzR8XqyhHXjoieAUoR7fBU6I5cfy1FVeh5GoRgriuzA==}
engines: {node: '>= 14.0.0'}
'@ampproject/remapping@2.3.0':
@@ -5805,8 +5805,8 @@ packages:
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
algoliasearch@5.57.0:
resolution: {integrity: sha512-HpND7MBGctOAkd1GoQoDZCGoCpqNTS5NG1LuhElFet3RdLJkwnyTYZXZhXwtpAQPrI36fqQ3eT6KQrdKDTKu3A==}
algoliasearch@5.59.0:
resolution: {integrity: sha512-wUXzaeI7B526W4y1gFg3lcxgDZ67XSgRJIiellYWOas/pLpO7rOtxm9Gr2E/C8aiSDXIgx1q5TdSsvK67Uakqw==}
engines: {node: '>= 14.0.0'}
alien-signals@3.2.1:
@@ -11511,117 +11511,117 @@ snapshots:
'@adobe/css-tools@4.5.0': {}
'@algolia/abtesting@1.23.0':
'@algolia/abtesting@1.25.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)(search-insights@2.8.2)':
'@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)(search-insights@2.8.2)':
dependencies:
'@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)(search-insights@2.8.2)
'@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)
'@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)(search-insights@2.8.2)
'@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
- search-insights
'@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)(search-insights@2.8.2)':
'@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)(search-insights@2.8.2)':
dependencies:
'@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)
'@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)
search-insights: 2.8.2
transitivePeerDependencies:
- '@algolia/client-search'
- algoliasearch
'@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)':
'@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)':
dependencies:
'@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)
'@algolia/client-search': 5.57.0
algoliasearch: 5.57.0
'@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)
'@algolia/client-search': 5.59.0
algoliasearch: 5.59.0
'@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)':
'@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)':
dependencies:
'@algolia/client-search': 5.57.0
algoliasearch: 5.57.0
'@algolia/client-search': 5.59.0
algoliasearch: 5.59.0
'@algolia/client-abtesting@5.57.0':
'@algolia/client-abtesting@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/client-analytics@5.57.0':
'@algolia/client-analytics@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/client-common@5.57.0': {}
'@algolia/client-common@5.59.0': {}
'@algolia/client-insights@5.57.0':
'@algolia/client-insights@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/client-personalization@5.57.0':
'@algolia/client-personalization@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/client-query-suggestions@5.57.0':
'@algolia/client-query-suggestions@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/client-search@5.57.0':
'@algolia/client-search@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/ingestion@1.57.0':
'@algolia/ingestion@1.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/monitoring@1.57.0':
'@algolia/monitoring@1.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/recommend@5.57.0':
'@algolia/recommend@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
'@algolia/requester-browser-xhr@5.57.0':
'@algolia/requester-browser-xhr@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-fetch@5.57.0':
'@algolia/requester-fetch@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/client-common': 5.59.0
'@algolia/requester-node-http@5.57.0':
'@algolia/requester-node-http@5.59.0':
dependencies:
'@algolia/client-common': 5.57.0
'@algolia/client-common': 5.59.0
'@ampproject/remapping@2.3.0':
dependencies:
@@ -13203,9 +13203,9 @@ snapshots:
'@docsearch/css@3.8.2': {}
'@docsearch/js@3.8.2(@algolia/client-search@5.57.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)':
'@docsearch/js@3.8.2(@algolia/client-search@5.59.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)':
dependencies:
'@docsearch/react': 3.8.2(@algolia/client-search@5.57.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)
'@docsearch/react': 3.8.2(@algolia/client-search@5.59.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)
preact: 10.29.8
transitivePeerDependencies:
- '@algolia/client-search'
@@ -13215,12 +13215,12 @@ snapshots:
- react-dom
- search-insights
'@docsearch/react@3.8.2(@algolia/client-search@5.57.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)':
'@docsearch/react@3.8.2(@algolia/client-search@5.59.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)':
dependencies:
'@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)(search-insights@2.8.2)
'@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.57.0)(algoliasearch@5.57.0)
'@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)(search-insights@2.8.2)
'@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.59.0)(algoliasearch@5.59.0)
'@docsearch/css': 3.8.2
algoliasearch: 5.57.0
algoliasearch: 5.59.0
optionalDependencies:
'@types/react': 18.3.31
react: 18.3.1
@@ -16378,22 +16378,22 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
algoliasearch@5.57.0:
algoliasearch@5.59.0:
dependencies:
'@algolia/abtesting': 1.23.0
'@algolia/client-abtesting': 5.57.0
'@algolia/client-analytics': 5.57.0
'@algolia/client-common': 5.57.0
'@algolia/client-insights': 5.57.0
'@algolia/client-personalization': 5.57.0
'@algolia/client-query-suggestions': 5.57.0
'@algolia/client-search': 5.57.0
'@algolia/ingestion': 1.57.0
'@algolia/monitoring': 1.57.0
'@algolia/recommend': 5.57.0
'@algolia/requester-browser-xhr': 5.57.0
'@algolia/requester-fetch': 5.57.0
'@algolia/requester-node-http': 5.57.0
'@algolia/abtesting': 1.25.0
'@algolia/client-abtesting': 5.59.0
'@algolia/client-analytics': 5.59.0
'@algolia/client-common': 5.59.0
'@algolia/client-insights': 5.59.0
'@algolia/client-personalization': 5.59.0
'@algolia/client-query-suggestions': 5.59.0
'@algolia/client-search': 5.59.0
'@algolia/ingestion': 1.59.0
'@algolia/monitoring': 1.59.0
'@algolia/recommend': 5.59.0
'@algolia/requester-browser-xhr': 5.59.0
'@algolia/requester-fetch': 5.59.0
'@algolia/requester-node-http': 5.59.0
alien-signals@3.2.1: {}
@@ -18332,7 +18332,8 @@ snapshots:
functions-have-names@1.2.3: {}
fuse.js@7.5.0: {}
fuse.js@7.5.0:
optional: true
generator-function@2.0.1:
optional: true
@@ -22708,10 +22709,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
vitepress@1.6.4(@algolia/client-search@5.57.0)(@types/node@26.4.1)(@types/react@18.3.31)(fuse.js@7.5.0)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(lightningcss@1.33.0)(postcss@8.5.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.103.1)(search-insights@2.8.2)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(typescript@6.0.3)(yaml@2.9.0):
vitepress@1.6.4(@algolia/client-search@5.59.0)(@types/node@26.4.1)(@types/react@18.3.31)(fuse.js@7.5.0)(jiti@2.7.0)(less@4.9.1(supports-color@10.2.2))(lightningcss@1.33.0)(postcss@8.5.27)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.103.1)(search-insights@2.8.2)(stylus@0.56.0(supports-color@10.2.2))(terser@5.51.2)(typescript@6.0.3)(yaml@2.9.0):
dependencies:
'@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.57.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)
'@docsearch/js': 3.8.2(@algolia/client-search@5.59.0)(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.8.2)
'@iconify-json/simple-icons': 1.2.94
'@shikijs/core': 2.5.0
'@shikijs/transformers': 2.5.0