docs: redesign search (#12628)

* docs: redesign search

* add shadow in dark mode
This commit is contained in:
Shahed Nasser
2025-05-28 10:01:04 +03:00
committed by GitHub
parent d155f492be
commit 9b3218885c
13 changed files with 157 additions and 200 deletions

View File

@@ -1,10 +1,6 @@
"use client"
import {
usePageLoading,
SearchProvider as UiSearchProvider,
searchFilters,
} from "docs-ui"
import { usePageLoading, SearchProvider as UiSearchProvider } from "docs-ui"
import { config } from "../config"
import basePathUrl from "../utils/base-path-url"
@@ -20,17 +16,18 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "temp",
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp",
mainIndexName: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
indices: [
{
name: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
{
name: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
],
}}
indices={[
{
value: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
value: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
]}
defaultIndex={process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp"}
searchProps={{
isLoading,
suggestions: [
@@ -55,7 +52,6 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
checkInternalPattern: new RegExp(
`^${config.baseUrl}${basePathUrl(`/(admin|store)`)}`
),
filterOptions: searchFilters,
}}
>
{children}

View File

@@ -1,6 +1,6 @@
"use client"
import { SearchProvider as UiSearchProvider, searchFilters } from "docs-ui"
import { SearchProvider as UiSearchProvider } from "docs-ui"
import { config } from "../config"
type SearchProviderProps = {
@@ -15,17 +15,18 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp",
mainIndexName:
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
indices: [
{
name: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
name: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
],
}}
indices={[
{
value: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
value: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
]}
defaultIndex={process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp"}
searchProps={{
isLoading: false,
suggestions: [
@@ -51,7 +52,6 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
checkInternalPattern: new RegExp(
`^${config.baseUrl}/([^(resources)])*`
),
filterOptions: searchFilters,
}}
>
{children}

View File

@@ -1,6 +1,6 @@
"use client"
import { SearchProvider as UiSearchProvider, searchFilters } from "docs-ui"
import { SearchProvider as UiSearchProvider } from "docs-ui"
import { config } from "../config"
type SearchProviderProps = {
@@ -15,17 +15,18 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp",
mainIndexName:
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
indices: [
{
name: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
name: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
],
}}
indices={[
{
value: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
value: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
]}
defaultIndex={process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp"}
searchProps={{
isLoading: false,
suggestions: [
@@ -40,7 +41,6 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
},
],
checkInternalPattern: new RegExp(`^${config.baseUrl}/resources/.*`),
filterOptions: searchFilters,
}}
>
{children}

View File

@@ -1,6 +1,6 @@
"use client"
import { SearchProvider as UiSearchProvider, searchFilters } from "docs-ui"
import { SearchProvider as UiSearchProvider } from "docs-ui"
import { absoluteUrl } from "../lib/absolute-url"
type SearchProviderProps = {
@@ -15,17 +15,18 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp",
mainIndexName:
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
indices: [
{
name: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
name: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
],
}}
indices={[
{
value: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
value: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
]}
defaultIndex={process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp"}
searchProps={{
isLoading: false,
suggestions: [
@@ -35,7 +36,6 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
},
],
checkInternalPattern: new RegExp(`^${absoluteUrl()}/ui`),
filterOptions: searchFilters,
}}
>
{children}

View File

@@ -1,6 +1,6 @@
"use client"
import { SearchProvider as UiSearchProvider, searchFilters } from "docs-ui"
import { SearchProvider as UiSearchProvider } from "docs-ui"
import { config } from "../config"
type SearchProviderProps = {
@@ -15,17 +15,18 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp",
mainIndexName:
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
indices: [
{
name: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
name: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
],
}}
indices={[
{
value: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
title: "Docs",
},
{
value: process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
title: "Store & Admin API",
},
]}
defaultIndex={process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp"}
searchProps={{
isLoading: false,
suggestions: [
@@ -43,7 +44,6 @@ const SearchProvider = ({ children }: SearchProviderProps) => {
},
],
checkInternalPattern: new RegExp(`^${config.baseUrl}/user-guide`),
filterOptions: searchFilters,
}}
>
{children}

View File

@@ -0,0 +1,31 @@
"use client"
import clsx from "clsx"
import React from "react"
import { useSearch } from "../../../providers"
export const SearchFilters = () => {
const { indices, selectedIndex, setSelectedIndex } = useSearch()
return (
<div className="pt-docs_0.75 px-docs_0.5 justify-center items-center w-full">
<div className="flex flex-wrap bg-medusa-bg-disabled rounded-docs_DEFAULT p-docs_0.125">
{indices.map((index) => (
<button
key={index.value}
className={clsx(
"text-compact-small-plus flex-1 p-docs_0.25",
selectedIndex === index.value && [
"rounded-docs_sm text-medusa-fg-base bg-medusa-bg-base",
"shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark",
],
selectedIndex !== index.value && "text-medusa-fg-muted"
)}
onClick={() => setSelectedIndex(index.value)}
>
{index.title}
</button>
))}
</div>
</div>
)
}

View File

@@ -8,13 +8,16 @@ export const SearchFooter = () => {
className={clsx(
"py-docs_0.75 hidden md:flex items-center justify-end px-docs_1",
"border-medusa-border-base border-t",
"bg-medusa-bg-field"
"bg-medusa-bg-field z-10"
)}
>
<div className="flex items-center gap-docs_0.75">
<div className="flex items-center gap-docs_0.5">
<span
className={clsx("text-medusa-fg-subtle", "text-compact-x-small")}
className={clsx(
"text-medusa-fg-subtle",
"text-compact-x-small-plus"
)}
>
Navigation
</span>
@@ -40,7 +43,10 @@ export const SearchFooter = () => {
<div className={clsx("h-docs_0.75 w-px bg-medusa-border-strong")}></div>
<div className="flex items-center gap-docs_0.5">
<span
className={clsx("text-medusa-fg-subtle", "text-compact-x-small")}
className={clsx(
"text-medusa-fg-subtle",
"text-compact-x-small-plus"
)}
>
Open Result
</span>

View File

@@ -1,6 +1,6 @@
"use client"
import React, { Fragment, useEffect, useMemo, useState } from "react"
import React, { useEffect, useMemo, useState } from "react"
import clsx from "clsx"
import {
Configure,
@@ -11,7 +11,7 @@ import {
useInstantSearch,
} from "react-instantsearch"
import { SearchNoResult } from "../NoResults"
import { AlgoliaIndex, useSearch } from "@/providers"
import { useSearch } from "@/providers"
import { Link, SearchHitGroupName } from "@/components"
export type Hierarchy = "lvl0" | "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5"
@@ -42,7 +42,6 @@ export type GroupedHitType = {
export type SearchHitWrapperProps = {
configureProps: ConfigureProps
indices: AlgoliaIndex[]
} & Omit<SearchHitsProps, "indexName" | "setNoResults">
export type IndexResults = {
@@ -51,41 +50,46 @@ export type IndexResults = {
export const SearchHitsWrapper = ({
configureProps,
indices,
...rest
}: SearchHitWrapperProps) => {
const { status } = useInstantSearch()
const [hasNoResults, setHashNoResults] = useState<IndexResults>({
[indices[0].name]: false,
[indices[1].name]: false,
const { selectedIndex, indices } = useSearch()
const [hasNoResults, setHasNoResults] = useState<IndexResults>({
[indices[0].value]: false,
[indices[1].value]: false,
})
const showNoResults = useMemo(() => {
return Object.values(hasNoResults).every((value) => value === true)
}, [hasNoResults])
const setNoResults = (index: string, value: boolean) => {
setHashNoResults((prev: IndexResults) => ({
setHasNoResults((prev) => ({
...prev,
[index]: value,
}))
}
const showNoResults = useMemo(() => {
return Object.values(hasNoResults).every((val) => val)
}, [hasNoResults])
return (
<div className="h-full overflow-auto px-docs_0.5">
<div className="overflow-auto px-docs_0.5 flex-1">
{status !== "loading" && showNoResults && <SearchNoResult />}
{indices.map((index, key) => (
// @ts-expect-error React v19 doesn't see this type as a React element
<Index indexName={index.name} key={key}>
{!hasNoResults[index.name] && (
<SearchHitGroupName name={index.title} />
)}
<SearchHits
indexName={index.name}
setNoResults={setNoResults}
{...rest}
/>
<Configure {...configureProps} />
</Index>
{indices.map((index) => (
<div
className={clsx(index.value !== selectedIndex && "hidden")}
key={index.value}
data-index
>
{/* @ts-expect-error React v19 doesn't see this type as a React element */}
<Index indexName={index.value}>
{!hasNoResults[index.value] && (
<SearchHitGroupName name={index.title} />
)}
<SearchHits
indexName={index.value}
setNoResults={setNoResults}
{...rest}
/>
<Configure {...configureProps} />
</Index>
</div>
))}
</div>
)

View File

@@ -1,16 +1,16 @@
"use client"
import React, { useEffect, useMemo, useRef, useState } from "react"
import React, { useEffect, useRef } from "react"
import { InstantSearch, SearchBox } from "react-instantsearch"
import clsx from "clsx"
import { SearchEmptyQueryBoundary } from "./EmptyQueryBoundary"
import { SearchSuggestions, type SearchSuggestionType } from "./Suggestions"
import { AlgoliaProps, useSearch } from "@/providers"
import { checkArraySameElms } from "@/utils"
import { SearchHitsWrapper } from "./Hits"
import { SelectBadge, SpinnerLoading } from "@/components"
import { SpinnerLoading } from "@/components"
import { useSearchNavigation, type OptionType } from "@/hooks"
import { SearchFooter } from "./Footer"
import { SearchFilters } from "./Filters"
export type SearchProps = {
algolia: AlgoliaProps
@@ -25,21 +25,13 @@ export const Search = ({
suggestions,
isLoading = false,
checkInternalPattern,
filterOptions = [],
}: SearchProps) => {
const { isOpen, defaultFilters, searchClient, modalRef } = useSearch()
const [filters, setFilters] = useState<string[]>(defaultFilters)
const { isOpen, searchClient, modalRef } = useSearch()
const searchBoxRef = useRef<HTMLFormElement>(null)
const focusSearchInput = () =>
searchBoxRef.current?.querySelector("input")?.focus()
useEffect(() => {
if (!checkArraySameElms(defaultFilters, filters)) {
setFilters(defaultFilters)
}
}, [defaultFilters])
useEffect(() => {
if (isOpen && searchBoxRef.current) {
focusSearchInput()
@@ -57,14 +49,6 @@ export const Search = ({
}
}, [isOpen])
const facetFilters = useMemo(() => {
const filtersToUse =
!filters.length || filters[0] === "all"
? filterOptions.map((option) => option.value)
: filters
return filtersToUse.map((filter) => "_tags:" + filter)
}, [filters, defaultFilters])
useSearchNavigation({
getInputElm: () =>
searchBoxRef.current?.querySelector("input") as HTMLInputElement,
@@ -76,30 +60,6 @@ export const Search = ({
return (
<div className="h-full flex flex-col">
{filterOptions.length > 0 && (
<SelectBadge
multiple
options={filterOptions}
value={filters}
setSelected={(value) =>
setFilters(Array.isArray(value) ? [...value] : [value])
}
addSelected={(value) => setFilters((prev) => [...prev, value])}
removeSelected={(value) =>
setFilters((prev) => prev.filter((v) => v !== value))
}
showClearButton={false}
placeholder="Filters"
handleAddAll={(isAllSelected: boolean) => {
if (isAllSelected) {
setFilters(defaultFilters)
} else {
setFilters(filterOptions.map((option) => option.value))
}
}}
className="px-docs_1 pt-docs_1 bg-medusa-bg-base z-10"
/>
)}
{/* @ts-expect-error React v19 doesn't see this type as a React element */}
<InstantSearch
indexName={algolia.mainIndexName}
@@ -143,26 +103,16 @@ export const Search = ({
</div>
<div
className={clsx(
"md:flex-initial",
filterOptions.length > 0 &&
"h-[calc(100%-95px)] lg:max-h-[calc(100%-140px)] lg:min-h-[calc(100%-140px)]",
filterOptions.length === 0 &&
"h-[calc(100%-75px)] lg:max-h-[calc(100%-100px)] lg:min-h-[calc(100%-100px)]"
"md:flex-initial flex flex-col",
"h-[calc(100%-75px)] lg:max-h-[calc(100%-100px)] lg:min-h-[calc(100%-100px)]"
)}
>
<SearchFilters />
<SearchEmptyQueryBoundary
fallback={<SearchSuggestions suggestions={suggestions} />}
>
<SearchHitsWrapper
configureProps={{
// filters array has to be wrapped
// in another array for an OR condition
// to be applied between the items.
facetFilters: [facetFilters],
getRankingInfo: true,
hitsPerPage: 3,
}}
indices={algolia.indices}
configureProps={{}}
checkInternalPattern={checkInternalPattern}
/>
</SearchEmptyQueryBoundary>

View File

@@ -82,6 +82,7 @@ export const VerticalCodeTabs = ({
{...tabs[selectedTabIndex].code}
noCopy={true}
noReport={true}
noAskAi={true}
forceNoTitle={true}
wrapperClassName="h-full !rounded-docs_DEFAULT"
className={clsx(

View File

@@ -1,4 +1,3 @@
import { OptionType } from "@/hooks"
import { NavigationItem } from "types"
export const GITHUB_ISSUES_LINK =
@@ -356,31 +355,3 @@ export const navDropdownItems: NavigationItem[] = [
link: "/user-guide",
},
]
export const searchFilters: OptionType[] = [
{
value: "concepts-guides",
label: "Concepts & Guides",
hitsPerPage: 8,
},
{
value: "references",
label: "References",
},
{
value: "admin-v2",
label: "Admin API",
},
{
value: "store-v2",
label: "Store API",
},
{
value: "user-guide",
label: "User Guide",
},
{
value: "troubleshooting",
label: "Troubleshooting",
},
]

View File

@@ -87,7 +87,7 @@ export const useSearchNavigation = ({
if (isInput) {
// go to the first data-hit item
const nextItem = modalRef.current?.querySelector(
"[data-hit]"
"[data-index]:not(.hidden) [data-hit]"
) as HTMLElement
nextItem?.focus()
} else {

View File

@@ -9,7 +9,6 @@ import React, {
useRef,
} from "react"
import { BadgeProps, Modal, Search, SearchProps } from "@/components"
import { checkArraySameElms } from "../../utils"
import {
liteClient as algoliasearch,
LiteClient as SearchClient,
@@ -30,20 +29,21 @@ export type SearchCommand = {
export type SearchContextType = {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
defaultFilters: string[]
setDefaultFilters: (value: string[]) => void
searchClient: SearchClient
commands: SearchCommand[]
command: SearchCommand | null
setCommand: React.Dispatch<React.SetStateAction<SearchCommand | null>>
setCommands: React.Dispatch<React.SetStateAction<SearchCommand[]>>
modalRef: React.MutableRefObject<HTMLDialogElement | null>
modalRef: React.RefObject<HTMLDialogElement | null>
indices: AlgoliaIndex[]
selectedIndex: string
setSelectedIndex: (value: string) => void
}
const SearchContext = createContext<SearchContextType | null>(null)
export type AlgoliaIndex = {
name: string
value: string
title: string
}
@@ -51,12 +51,12 @@ export type AlgoliaProps = {
appId: string
apiKey: string
mainIndexName: string
indices: AlgoliaIndex[]
}
export type SearchProviderProps = {
children: React.ReactNode
initialDefaultFilters?: string[]
indices: AlgoliaIndex[]
defaultIndex: string
algolia: AlgoliaProps
searchProps: Omit<SearchProps, "algolia">
commands?: SearchCommand[]
@@ -65,16 +65,16 @@ export type SearchProviderProps = {
export const SearchProvider = ({
children,
initialDefaultFilters = [],
defaultIndex: initialDefaultIndex,
searchProps,
algolia,
commands: initialCommands = [],
modalClassName,
indices,
}: SearchProviderProps) => {
const [isOpen, setIsOpen] = useState(false)
const [defaultFilters, setDefaultFilters] = useState<string[]>(
initialDefaultFilters
)
const [selectedIndex, setSelectedIndex] =
useState<string>(initialDefaultIndex)
const [commands, setCommands] = useState<SearchCommand[]>(initialCommands)
const [command, setCommand] = useState<SearchCommand | null>(null)
@@ -88,13 +88,10 @@ export const SearchProvider = ({
}, [algolia.appId, algolia.apiKey])
useEffect(() => {
if (
initialDefaultFilters.length &&
!checkArraySameElms(defaultFilters, initialDefaultFilters)
) {
setDefaultFilters(initialDefaultFilters)
if (initialDefaultIndex !== selectedIndex) {
setSelectedIndex(initialDefaultIndex)
}
}, [initialDefaultFilters])
}, [initialDefaultIndex])
const componentWrapperRef = useRef(null)
@@ -107,14 +104,15 @@ export const SearchProvider = ({
value={{
isOpen,
setIsOpen,
defaultFilters,
setDefaultFilters,
searchClient,
commands,
command,
setCommand,
modalRef,
setCommands,
indices,
selectedIndex,
setSelectedIndex,
}}
>
{children}