diff --git a/components/Common/OramaSearchBar/OramaLogo.tsx b/components/Common/OramaSearchBar/OramaLogo.tsx
new file mode 100644
index 0000000000000..530ea0fb7e31d
--- /dev/null
+++ b/components/Common/OramaSearchBar/OramaLogo.tsx
@@ -0,0 +1,695 @@
+type OramaLogoProps = {
+ className?: string;
+};
+
+// Keep up to date with: https://docs.oramasearch.com/logo/logo-orama-dark.svg
+const OramaLogoDark = ({ className }: OramaLogoProps): JSX.Element => {
+ return (
+
+ );
+};
+
+// Keep up to date with: https://docs.oramasearch.com/logo/logo-orama-light.svg
+const OramaLogoLight = ({ className }: OramaLogoProps): JSX.Element => {
+ return (
+
+ );
+};
+
+export { OramaLogoLight, OramaLogoDark };
diff --git a/components/Common/OramaSearchBar/__snapshots__/index.stories.tsx.snap b/components/Common/OramaSearchBar/__snapshots__/index.stories.tsx.snap
new file mode 100644
index 0000000000000..8d0fc314c1571
--- /dev/null
+++ b/components/Common/OramaSearchBar/__snapshots__/index.stories.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Common/OramaSearchBar Default smoke-test 1`] = `
+
+`;
diff --git a/components/Common/OramaSearchBar/index.module.scss b/components/Common/OramaSearchBar/index.module.scss
new file mode 100644
index 0000000000000..73a34df327b0a
--- /dev/null
+++ b/components/Common/OramaSearchBar/index.module.scss
@@ -0,0 +1,24 @@
+.oramaSearchBarFooter {
+ align-items: center;
+ column-gap: 1ch;
+ display: flex;
+ justify-content: end;
+ margin: 5px;
+
+ .oramaSearchBarFooterLabel {
+ color: var(--black6);
+ font-size: 0.8em;
+ margin-top: -2px;
+ white-space: nowrap;
+ }
+
+ .oramaSearchBarFooterLink {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ }
+
+ .oramaSearchBarFooterLogo {
+ min-width: 6em;
+ }
+}
diff --git a/components/Common/OramaSearchBar/index.stories.tsx b/components/Common/OramaSearchBar/index.stories.tsx
new file mode 100644
index 0000000000000..0967fc6bb806b
--- /dev/null
+++ b/components/Common/OramaSearchBar/index.stories.tsx
@@ -0,0 +1,50 @@
+import { create, insertMultiple, save } from '@orama/orama';
+import OramaSearchBar from './index';
+
+import type { Meta as MetaObj, StoryObj } from '@storybook/react';
+import type { Schema } from '@orama/orama';
+
+type Story = StoryObj;
+type Meta = MetaObj;
+
+const schema: Schema = { title: 'string', displayTitle: 'string' };
+const documents = [
+ {
+ id: 'voluptate',
+ slug: 'voluptate',
+ title: 'Nisi deserunt excepteur',
+ category: 'aliqua-sunt',
+ displayTitle: 'Adipisicing magna irure elit velit.',
+ },
+ {
+ id: 'ullamco',
+ slug: 'ullamco',
+ title: 'Lorem adipisicing ut',
+ category: 'excepteur',
+ displayTitle: 'Aliqua voluptate aliqua non consectetur sunt consequat.',
+ },
+ {
+ id: 'deserunt',
+ slug: 'deserunt',
+ title: 'Qui irure do irure',
+ category: 'laborum',
+ wrapInCode: true,
+ },
+];
+
+export const Default: Story = {
+ loaders: [
+ async () => {
+ const database = await create({ schema });
+ await insertMultiple(database, documents);
+ return { orama: await save(database) };
+ },
+ ],
+};
+
+export default {
+ component: OramaSearchBar,
+ render: (args, { loaded: { orama } }) => {
+ return ;
+ },
+} as Meta;
diff --git a/components/Common/OramaSearchBar/index.tsx b/components/Common/OramaSearchBar/index.tsx
new file mode 100644
index 0000000000000..fae237e005e07
--- /dev/null
+++ b/components/Common/OramaSearchBar/index.tsx
@@ -0,0 +1,64 @@
+import { create, load } from '@orama/orama';
+import { useTheme } from 'next-themes';
+import { useCallback, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { OramaLogoDark, OramaLogoLight } from './OramaLogo';
+import styles from './index.module.scss';
+import { identityDocumentMapper, performSearch } from './search';
+import SearchBar from '../SearchBar';
+
+import type { FC } from 'react';
+import type { OramaSearchBarProps } from './search';
+import type { SearchFunction } from '../../../types';
+
+const Footer: FC = () => {
+ const { resolvedTheme: theme } = useTheme();
+
+ const isDark = theme === 'dark';
+
+ return (
+
+ );
+};
+
+const OramaSearchBar: FC = ({
+ schema,
+ index,
+ documentMapper,
+}: OramaSearchBarProps) => {
+ const [searchFunction, setSearchFunction] = useState();
+
+ // Create a setup function to deserialize the database
+ const setup = useCallback(async () => {
+ const database = await create({ schema });
+ await load(database, index);
+
+ setSearchFunction(() =>
+ performSearch.bind(
+ null,
+ database,
+ documentMapper ?? identityDocumentMapper
+ )
+ );
+ }, [schema, index, documentMapper, setSearchFunction]);
+
+ return ;
+};
+
+export default OramaSearchBar;
diff --git a/components/Common/OramaSearchBar/search.ts b/components/Common/OramaSearchBar/search.ts
new file mode 100644
index 0000000000000..c3b3891bc1ac2
--- /dev/null
+++ b/components/Common/OramaSearchBar/search.ts
@@ -0,0 +1,26 @@
+import { search } from '@orama/orama';
+
+import type { Document, Orama, RawData, Schema } from '@orama/orama';
+import type { SearchResult } from '../../../types';
+
+export type DocumentMapper = (doc: Document) => SearchResult;
+
+export type OramaSearchBarProps = {
+ schema: Schema;
+ index: RawData;
+ documentMapper?: DocumentMapper;
+};
+
+export const identityDocumentMapper = (doc: Document): SearchResult => {
+ return doc as unknown as SearchResult;
+};
+
+export const performSearch = async (
+ database: Orama,
+ mapper: DocumentMapper,
+ term: string
+): Promise => {
+ const results = await search(database, { term });
+
+ return results.hits.map(h => mapper(h.document));
+};
diff --git a/components/Common/SearchBar/__snapshots__/index.stories.tsx.snap b/components/Common/SearchBar/__snapshots__/index.stories.tsx.snap
new file mode 100644
index 0000000000000..59f5c0602a2ee
--- /dev/null
+++ b/components/Common/SearchBar/__snapshots__/index.stories.tsx.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Common/SearchBar Default smoke-test 1`] = `
+
+`;
diff --git a/components/Common/SearchBar/index.module.scss b/components/Common/SearchBar/index.module.scss
new file mode 100644
index 0000000000000..7bab2ac244c9c
--- /dev/null
+++ b/components/Common/SearchBar/index.module.scss
@@ -0,0 +1,136 @@
+.searchBarContainer {
+ background-color: var(--color-fill-top-nav);
+ border-radius: 6px;
+ box-shadow: none;
+ display: flex;
+ flex-direction: column;
+ max-height: 50vh;
+ max-width: 450px;
+ min-height: min-content;
+ min-width: min-content;
+ overflow-y: hidden;
+
+ &.expanded {
+ box-shadow: 0px 2px 12px 3px rgba(153, 204, 125, 0.14);
+ }
+
+ .searchInputContainer {
+ align-items: center;
+ display: flex;
+ position: relative;
+ width: 100%;
+
+ .searchIcon,
+ .closeIcon {
+ color: var(--color-text-accent);
+ padding: 1rem;
+ vertical-align: middle;
+ }
+
+ .closeIcon {
+ padding: 0 1rem 0 0;
+ }
+
+ .searchLabel {
+ align-items: center;
+ color: var(--color-text-accent);
+ cursor: pointer;
+ display: flex;
+ flex: 1;
+ font-weight: var(--font-weight-semibold);
+ transition: all 250ms ease-in-out;
+ width: 100%;
+ }
+
+ .searchInput {
+ background-color: transparent;
+ border: none;
+ border-radius: 6px;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-body1);
+ font-weight: var(--font-weight-semibold);
+ height: 100%;
+ outline: none;
+ width: 100%;
+
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+
+ .searchResults {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 5em;
+ overflow-y: auto;
+ width: 100%;
+
+ &.noResults {
+ justify-content: center;
+ }
+
+ .loadingMessage {
+ align-self: center;
+ color: var(--color-text-primary);
+ display: flex;
+ font-size: 14px;
+ justify-content: center;
+ justify-self: center;
+ white-space: nowrap;
+ }
+ }
+
+ .searchResultsList {
+ list-style: circle;
+ margin: 0;
+ padding: 0 0 0 var(--space-32);
+ }
+
+ .searchResultsFooter {
+ border-top: var(--space-01) solid var(--black2);
+ }
+
+ .searchResult {
+ color: var(--color-text-primary);
+ display: list-item;
+ margin: 0;
+ padding: 3px;
+
+ &:hover {
+ font-weight: bold;
+ }
+
+ .searchResultLink {
+ display: block;
+ padding: 2px;
+ text-decoration: none;
+ text-overflow: ellipsis;
+
+ &:focus {
+ outline: var(--brand3) dotted 2px;
+ }
+ }
+
+ .searchResultText {
+ text-transform: capitalize;
+ }
+
+ .searchResultCode {
+ border-radius: 0.3rem;
+ font-weight: var(--font-weight-light);
+ padding: 0 6px;
+ }
+ }
+}
+
+[data-theme='light'] .searchResultCode {
+ background-color: var(--black2);
+ color: var(--black9);
+}
+
+[data-theme='dark'] .searchResultCode {
+ background-color: var(--black9);
+ color: var(--black2);
+}
diff --git a/components/Common/SearchBar/index.stories.tsx b/components/Common/SearchBar/index.stories.tsx
new file mode 100644
index 0000000000000..af290e90aa206
--- /dev/null
+++ b/components/Common/SearchBar/index.stories.tsx
@@ -0,0 +1,39 @@
+import SearchBar from './index';
+
+import type { Meta as MetaObj, StoryObj } from '@storybook/react';
+
+type Story = StoryObj;
+type Meta = MetaObj;
+
+export const Default: Story = {
+ args: {
+ search() {
+ return [
+ {
+ id: 'voluptate',
+ slug: 'voluptate',
+ title: 'Nisi deserunt excepteur',
+ category: 'aliqua-sunt',
+ displayTitle: 'Adipisicing magna irure elit velit.',
+ },
+ {
+ id: 'ullamco',
+ slug: 'ullamco',
+ title: 'Lorem adipisicing ut',
+ category: 'excepteur',
+ displayTitle:
+ 'Aliqua voluptate aliqua non consectetur sunt consequat.',
+ },
+ {
+ id: 'deserunt',
+ slug: 'deserunt',
+ title: 'Qui irure do irure',
+ category: 'laborum',
+ wrapInCode: true,
+ },
+ ];
+ },
+ },
+};
+
+export default { component: SearchBar } as Meta;
diff --git a/components/Common/SearchBar/index.tsx b/components/Common/SearchBar/index.tsx
new file mode 100644
index 0000000000000..bddbe8e22d55d
--- /dev/null
+++ b/components/Common/SearchBar/index.tsx
@@ -0,0 +1,337 @@
+import { MdTravelExplore, MdClose } from 'react-icons/md';
+import classNames from 'classnames';
+import { AnimatePresence, motion } from 'framer-motion';
+import Link from 'next/link';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { FormattedMessage } from 'react-intl';
+import styles from './index.module.scss';
+import SectionTitle from '../SectionTitle';
+import { useClickOutside } from '../../../hooks/useClickOutside';
+import useKeyPress from '../../../hooks/useKeypress';
+import type { FC } from 'react';
+import type { SearchFunction, SearchResult } from '../../../types';
+
+type SearchBarProps = {
+ search?: SearchFunction;
+ setup?: () => Promise;
+ footer?: FC | (() => JSX.Element);
+};
+
+const containerTransition = { type: 'spring', damping: 22, stiffness: 150 };
+
+const containerVariants = {
+ expanded: {
+ width: 'auto',
+ height: 'auto',
+ },
+ collapsed: {
+ width: 0,
+ height: '3em',
+ },
+};
+
+const SearchBar: FC = ({
+ setup,
+ search,
+ footer: Footer,
+}: SearchBarProps) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const searchInputRef = useRef(null);
+ const listRef = useRef(null);
+ const activeResultRef = useRef(-1);
+
+ const [isExpanded, setExpanded] = useState(false);
+ const [query, setQuery] = useState('');
+ const [results, setResults] = useState([]);
+ const isEmpty = !results || results.length === 0;
+
+ const containerClassNames = classNames(styles.searchBarContainer, {
+ [styles.expanded]: isExpanded,
+ });
+
+ const resultsClassNames = classNames(styles.searchResults, {
+ [styles.noResults]: isEmpty,
+ });
+
+ const expandContainer = useCallback(() => {
+ setExpanded(true);
+ }, [setExpanded]);
+
+ const collapseContainer = useCallback(() => {
+ setQuery('');
+ setExpanded(false);
+ }, [setQuery, setExpanded]);
+
+ const selectItem = useCallback(
+ (index: number) => {
+ if (!listRef.current) {
+ return;
+ }
+
+ const el = listRef.current.children[index]!;
+ el.querySelector('a')?.focus();
+ el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ },
+ [listRef]
+ );
+
+ const toggleContainer = useCallback(() => {
+ isExpanded ? collapseContainer() : expandContainer();
+ }, [isExpanded, collapseContainer, expandContainer]);
+
+ const onKeyPressHandler = useCallback(() => {
+ if (!isExpanded) {
+ expandContainer();
+ }
+ }, [isExpanded, expandContainer]);
+
+ const onQueryChangeHandler = useCallback(
+ (e: React.ChangeEvent) => {
+ e.preventDefault();
+
+ // Clear results when the query is emptied
+ if (!e.target.value) {
+ setResults([]);
+ }
+
+ setQuery(e.target.value);
+ },
+ [setQuery, setResults]
+ );
+
+ const onBlurHandler = useCallback(
+ (e: React.FocusEvent) => {
+ if (!e.currentTarget.contains(e.target) || isEmpty) {
+ collapseContainer();
+ }
+ },
+ [isEmpty, collapseContainer]
+ );
+
+ const onCloseHandler = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (isExpanded) {
+ collapseContainer();
+ }
+ },
+ [isExpanded, collapseContainer]
+ );
+
+ const onSearchResultKeyHandler = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (!isExpanded || isEmpty || !listRef.current) {
+ return;
+ }
+
+ const currentResultIndex = activeResultRef.current;
+ const maxResultIndex = listRef.current.children.length - 1;
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+
+ activeResultRef.current =
+ currentResultIndex + 1 > maxResultIndex ? 0 : currentResultIndex + 1;
+ selectItem(activeResultRef.current);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+
+ activeResultRef.current =
+ currentResultIndex - 1 < 0 ? maxResultIndex : currentResultIndex - 1;
+ selectItem(activeResultRef.current);
+ }
+ },
+ [isExpanded, isEmpty, activeResultRef, selectItem]
+ );
+
+ const parentRef = useClickOutside(collapseContainer);
+
+ // Focus the field when expanding the bar
+ useEffect(() => {
+ if (isExpanded) {
+ searchInputRef.current?.focus();
+ }
+ }, [isExpanded, searchInputRef]);
+
+ // Perform the setup call, if any
+ useEffect(() => {
+ if (!setup) {
+ setIsLoading(false);
+ return;
+ }
+
+ setup()
+ .then(() => {
+ setIsLoading(false);
+ })
+ .catch((error: Error) => {
+ console.error('Cannot load search data.', error);
+ });
+ }, [setup, setIsLoading]);
+
+ // Perform a search when the query changes
+ useEffect(() => {
+ if (isLoading || !query || !search) {
+ return;
+ }
+
+ try {
+ const searchReturnValue = search(query);
+
+ if (
+ typeof (searchReturnValue as Promise).then ===
+ 'function'
+ ) {
+ (searchReturnValue as Promise)
+ .then(r => setResults(r))
+ .catch((error: Error) => {
+ console.error('Cannot perform async search.', error);
+ });
+ } else {
+ setResults(searchReturnValue as SearchResult[]);
+ }
+ } catch (error) {
+ console.error('Cannot perform search.', error);
+ }
+ }, [isLoading, query, search, setResults]);
+
+ useKeyPress({
+ targetKey: 'ctrl+k',
+ callback: toggleContainer,
+ preventDefault: true,
+ });
+
+ useKeyPress({
+ targetKey: 'meta+k',
+ callback: toggleContainer,
+ preventDefault: true,
+ });
+
+ useKeyPress({
+ targetKey: 'Escape',
+ callback() {
+ if (isExpanded) {
+ collapseContainer();
+ }
+ },
+ preventDefault: true,
+ });
+
+ return (
+
+
+
+
+
+ {isExpanded && (
+
+
+
+ )}
+
+
+
+ {isExpanded && (
+ <>
+
+ {isLoading && (
+
+
+
+ )}
+
+ {!isLoading && isEmpty && (
+
+
+
+ )}
+ {!isLoading && !isEmpty && (
+
+ {results.map((result: SearchResult) => {
+ const sectionPath =
+ result.category === 'api'
+ ? ['home', result.category, result.title]
+ : ['home', result.category];
+
+ const displayTitle = result.displayTitle || result.title;
+
+ return (
+ -
+
+ {displayTitle &&
+ (result.wrapInCode ? (
+
+ {displayTitle}
+
+ ) : (
+
+ {displayTitle}
+
+ ))}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ {Footer && (
+
+
+
+ )}
+ >
+ )}
+
+ );
+};
+
+export default SearchBar;
diff --git a/hooks/useKeypress.ts b/hooks/useKeypress.ts
new file mode 100644
index 0000000000000..c443ebc726dd0
--- /dev/null
+++ b/hooks/useKeypress.ts
@@ -0,0 +1,60 @@
+/**
+ * A hook for detecting key presses.
+ */
+import { useEffect, useState } from 'react';
+
+const decorateKey = ({ metaKey, ctrlKey, key }: KeyboardEvent) => {
+ if (metaKey) {
+ return `meta+${key}`;
+ }
+
+ if (ctrlKey) {
+ return `ctrl+${key}`;
+ }
+
+ return key;
+};
+
+interface UseKeyPressProps {
+ targetKey: string;
+ callback?: () => void;
+ preventDefault?: boolean;
+}
+
+function useKeyPress({
+ targetKey,
+ callback,
+ preventDefault = false,
+}: UseKeyPressProps): boolean {
+ const [keyPressed, setKeyPressed] = useState(false);
+
+ useEffect(() => {
+ const downHandler = (e: KeyboardEvent) => {
+ if (decorateKey(e) === targetKey) {
+ if (preventDefault) {
+ e.preventDefault();
+ }
+ setKeyPressed(true);
+ if (typeof callback === 'function') {
+ callback();
+ }
+ }
+ };
+
+ const upHandler = (e: KeyboardEvent) => {
+ if (decorateKey(e) === targetKey) {
+ setKeyPressed(false);
+ }
+ };
+
+ window.addEventListener('keydown', downHandler);
+ window.addEventListener('keyup', upHandler);
+ return () => {
+ window.removeEventListener('keydown', downHandler);
+ window.removeEventListener('keyup', upHandler);
+ };
+ }, [callback, targetKey, preventDefault]);
+ return keyPressed;
+}
+
+export default useKeyPress;
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 7f42d271d3f8a..c911190d9fc66 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -78,5 +78,10 @@
"components.api.apiChanges.addedIn": "Added in: {version}",
"components.api.apiChanges.history": "History",
"components.api.apiChanges.history.version": "Version",
- "components.api.apiChanges.history.changes": "Changes"
+ "components.api.apiChanges.history.changes": "Changes",
+ "components.searchBar.placeholder": "Search",
+ "components.searchBar.search.title": "Start typing to search",
+ "components.searchBar.search.loading": "Loading search data ...",
+ "components.searchBar.search.noResults": "No results match your query",
+ "components.oramaSearchBar.search.poweredBy": "Search by"
}
diff --git a/package-lock.json b/package-lock.json
index dcaadfef327e8..077ff3dcba674 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,7 @@
"@mdx-js/react": "^2.3.0",
"@mui/material": "^5.13.6",
"@nodevu/core": "^0.1.0",
- "@types/node": "^18.16.19",
+ "@orama/orama": "^1.0.8",
"@vercel/analytics": "^1.0.1",
"classnames": "^2.3.2",
"cross-env": "^7.0.3",
@@ -55,6 +55,7 @@
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.2",
"@types/mdx": "^2.0.5",
+ "@types/node": "^18.16.19",
"@types/prismjs": "^1.26.0",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
@@ -5249,6 +5250,14 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/@orama/orama": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@orama/orama/-/orama-1.0.8.tgz",
+ "integrity": "sha512-UJvfnhCL4m4NQZFTs2kUDel67HHkzWCZJqF6Q4Dsx8hROXP82OXvPXsTXfHhHIyxtETdliEEkub1c6k6+36Gqw==",
+ "engines": {
+ "node": ">= 16.0.0"
+ }
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
diff --git a/package.json b/package.json
index 9e8e563a27df8..57179929ba4c1 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"@mdx-js/react": "^2.3.0",
"@mui/material": "^5.13.6",
"@nodevu/core": "^0.1.0",
- "@types/node": "^18.16.19",
+ "@orama/orama": "^1.0.8",
"@vercel/analytics": "^1.0.1",
"classnames": "^2.3.2",
"cross-env": "^7.0.3",
@@ -84,6 +84,7 @@
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.2",
"@types/mdx": "^2.0.5",
+ "@types/node": "^18.16.19",
"@types/prismjs": "^1.26.0",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
diff --git a/types/index.ts b/types/index.ts
index 36de67ba63c56..c21a4e74acbf5 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -6,7 +6,8 @@ export * from './features';
export * from './frontmatter';
export * from './i18n';
export * from './layouts';
+export * from './middlewares';
export * from './navigation';
export * from './prevNextLink';
export * from './releases';
-export * from './middlewares';
+export * from './search';
diff --git a/types/search.ts b/types/search.ts
new file mode 100644
index 0000000000000..edd45fb3dc6af
--- /dev/null
+++ b/types/search.ts
@@ -0,0 +1,12 @@
+export type SearchResult = {
+ id: React.Key | null | undefined;
+ slug: string;
+ title: string;
+ category: string;
+ displayTitle?: string | JSX.Element;
+ wrapInCode?: boolean;
+};
+
+export type SearchFunction = (
+ query: string
+) => SearchResult[] | Promise;