docs,api-ref: added search filters (#4830)

* initial implementation of search modal

* added hit and search suggestions

* added support for multiple indices

* updated sample env

* added close when click outside dropdown

* test for mobile

* added mobile design

* added shortcut

* dark mode fixes

* added search to docs

* added plugins filter

* added React import

* moved filters to configurations

* handled error on page load

* change suggestion text

* removed hits limit

* handle select all

* open link in current tab

* change highlight colors

* added support for shortcuts + auto focus

* change header and footer

* redesigned search ui
This commit is contained in:
Shahed Nasser
2023-08-24 18:36:06 +03:00
committed by GitHub
parent f4bf9ee169
commit f07dc0384f
109 changed files with 4555 additions and 1648 deletions

View File

@@ -3,14 +3,24 @@ import clsx from "clsx"
export type BadgeProps = {
className?: string
variant: string
variant:
| "purple"
| "purple-dark"
| "orange"
| "orange-dark"
| "green"
| "green-dark"
| "blue"
| "blue-dark"
| "red"
| "neutral"
} & React.HTMLAttributes<HTMLSpanElement>
const Badge: React.FC<BadgeProps> = ({ className, variant, children }) => {
return (
<span
className={clsx(
"text-compact-x-small-plus py-px px-0.4 rounded-sm border border-solid text-center",
"text-compact-x-small-plus px-0.4 rounded-sm border border-solid py-px text-center",
variant === "purple" &&
"bg-medusa-tag-purple-bg dark:bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text dark:text-medusa-tag-purple-text-dark border-medusa-tag-purple-border dark:border-medusa-tag-purple-border-dark",
variant === "purple-dark" &&
@@ -27,6 +37,10 @@ const Badge: React.FC<BadgeProps> = ({ className, variant, children }) => {
"bg-medusa-tag-blue-bg dark:bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text dark:text-medusa-tag-blue-text-dark border-medusa-tag-blue-border dark:border-medusa-tag-blue-border-dark",
variant === "blue-dark" &&
"bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text-dark border-medusa-tag-blue-border-dark",
variant === "red" &&
"bg-medusa-tag-red-bg dark:bg-medusa-tag-red-bg-dark text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark border-medusa-tag-red-border dark:border-medusa-tag-red-border-dark",
variant === "neutral" &&
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"badge",
className
)}

View File

@@ -2,6 +2,7 @@ import React from "react"
import clsx from "clsx"
export type ButtonProps = {
variant?: "secondary" | "primary" | "clear"
btnTypeClassName?: string
className?: string
onClick?: React.MouseEventHandler<HTMLButtonElement>
@@ -9,6 +10,7 @@ export type ButtonProps = {
} & React.HTMLAttributes<HTMLButtonElement>
const Button: React.FC<ButtonProps> = ({
variant,
className = "",
btnTypeClassName,
onClick,
@@ -18,7 +20,10 @@ const Button: React.FC<ButtonProps> = ({
return (
<button
className={clsx(
!btnTypeClassName?.length && "btn-secondary",
!variant && "btn-secondary",
variant === "primary" && "btn-primary",
variant === "secondary" && "btn-secondary",
variant === "clear" && "btn-clear",
btnTypeClassName,
className
)}

View File

@@ -13,7 +13,7 @@ const InputText: React.FC<InputTextProps> = (props) => {
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"rounded border-medusa-border-loud-muted dark:border-medusa-border-loud-muted-dark border border-solid",
"rounded border-medusa-border-base dark:border-medusa-border-base-dark border border-solid",
"px-0.75 py-[9px]",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"focus:border-medusa-border-interactive dark:focus:border-medusa-border-interactive-dark",

View File

@@ -14,7 +14,7 @@ const LearningPathIcon: React.FC<LearningPathIconProps> = ({
return (
<div
className={clsx(
"rounded-full shadow-card-rest dark:shadow-card-rest-dark w-3 h-3 bg-docs-button-neutral dark:bg-docs-button-neutral-dark",
"rounded-full shadow-card-rest dark:shadow-card-rest-dark w-3 h-3 bg-medusa-bg-base dark:bg-medusa-bg-base-dark",
"flex justify-center items-center flex-none",
className
)}

View File

@@ -25,12 +25,12 @@ const LearningPathStepActions: React.FC<LearningPathStepActionsType> = ({
<div className="flex gap-0.5 p-1 justify-end items-center">
<Button onClick={onClose}>Close</Button>
{hasNextStep() && (
<Button onClick={nextStep} btnTypeClassName="btn-primary">
<Button onClick={nextStep} variant="primary">
Next
</Button>
)}
{!hasNextStep() && (
<Button onClick={handleFinish} btnTypeClassName="btn-primary">
<Button onClick={handleFinish} variant="primary">
Finish
</Button>
)}

View File

@@ -0,0 +1,29 @@
import React from "react"
import clsx from "clsx"
import Button, { ButtonProps } from "../../Button"
type ModalFooterProps = {
actions?: ButtonProps[]
children?: React.ReactNode
className?: string
}
const ModalFooter = ({ actions, className, children }: ModalFooterProps) => {
return (
<div
className={clsx(
"py-1.5 pl-0 pr-2",
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-t border-solid",
"flex justify-end gap-0.5",
className
)}
>
{actions?.map((action, index) => (
<Button {...action} key={index} />
))}
{children}
</div>
)
}
export default ModalFooter

View File

@@ -0,0 +1,39 @@
import React from "react"
import clsx from "clsx"
import { useModal } from "../../../providers/Modal"
import Button from "../../Button"
import IconXMark from "../../../theme/Icon/XMark"
type ModalHeaderProps = {
title?: string
}
const ModalHeader = ({ title }: ModalHeaderProps) => {
const { closeModal } = useModal()
return (
<div
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-b border-solid py-1.5 px-2",
"flex items-center justify-between"
)}
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-h2"
)}
>
{title}
</span>
<Button
variant="clear"
className="cursor-pointer"
onClick={() => closeModal()}
>
<IconXMark />
</Button>
</div>
)
}
export default ModalHeader

View File

@@ -1,73 +1,117 @@
import clsx from "clsx"
import React from "react"
import Button, { ButtonProps } from "../Button"
import IconXMark from "../../theme/Icon/XMark"
import React, { forwardRef, useCallback, useEffect, useRef } from "react"
import { ButtonProps } from "../Button"
import ModalHeader from "./Header"
import ModalFooter from "./Footer"
import useKeyboardShortcut from "../../hooks/use-keyboard-shortcut"
import { useModal } from "../../providers/Modal"
export type ModalProps = {
className?: string
title?: string
actions?: ButtonProps[]
} & React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDialogElement>,
HTMLDialogElement
>
modalContainerClassName?: string
contentClassName?: string
onClose?: React.ReactEventHandler<HTMLDialogElement>
open?: boolean
footerContent?: React.ReactNode
} & Omit<React.ComponentProps<"dialog">, "ref">
const Modal = forwardRef<HTMLDialogElement, ModalProps>(function Modal(
{
className,
title,
actions,
children,
contentClassName,
modalContainerClassName,
onClose,
open = true,
footerContent,
...props
},
passedRef
) {
const { closeModal } = useModal()
const ref = useRef<HTMLDialogElement | null>(null)
const setRefs = useCallback(
(node: HTMLDialogElement) => {
// Ref's from useRef needs to have the node assigned to `current`
ref.current = node
if (typeof passedRef === "function") {
passedRef(node)
} else if (passedRef && "current" in passedRef) {
passedRef.current = node
}
},
[passedRef]
)
useKeyboardShortcut({
metakey: false,
checkEditing: false,
shortcutKeys: ["escape"],
action: () => {
if (open) {
ref.current?.close()
}
},
})
const handleClick = (e: React.MouseEvent<HTMLDialogElement, MouseEvent>) => {
// close modal when the user clicks outside the content
if (e.target === ref.current) {
closeModal()
onClose?.(e)
}
}
const handleClose = (e: React.SyntheticEvent<HTMLDialogElement, Event>) => {
onClose?.(e)
closeModal()
}
useEffect(() => {
if (open) {
document.body.setAttribute("data-modal", "opened")
} else {
document.body.removeAttribute("data-modal")
}
}, [open])
const Modal: React.FC<ModalProps> = ({
className,
title,
actions,
children,
...props
}) => {
return (
<dialog
{...props}
className={clsx(
"absolute top-0 left-0 flex w-screen h-screen justify-center items-center",
"",
"fixed top-0 left-0 flex h-screen w-screen items-center justify-center",
"bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark z-[500]",
"hidden open:flex border-0 p-0",
className
)}
onClick={handleClick}
ref={setRefs}
onClose={handleClose}
open={open}
>
<div
className={clsx(
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded-sm",
"border-medusa-border-base dark:border-medusa-border-base-dark border border-solid",
"shadow-modal dark:shadow-modal-dark",
"w-[90%] md:w-[75%] lg:w-1/2"
"w-[90%] md:h-auto md:w-[75%] lg:w-[560px]",
modalContainerClassName
)}
>
<div
className={clsx(
"py-1.5 px-2 border-0 border-solid border-b border-medusa-border-base dark:border-medusa-border-base-dark",
"flex justify-between items-center"
)}
>
<span
className={clsx(
"text-h1 text-medusa-fg-base dark:text-medusa-fg-base-dark"
)}
>
{title}
</span>
<IconXMark />
{title && <ModalHeader title={title} />}
<div className={clsx("overflow-auto py-1.5 px-2", contentClassName)}>
{children}
</div>
<div>{children}</div>
{actions && actions?.length > 0 && (
<div
className={clsx(
"pl-0 pr-2 px-1.5",
"border-0 border-solid border-t border-medusa-border-base dark:border-medusa-border-base-dark",
"flex justify-end gap-0.5"
)}
>
{actions.map((action, index) => (
<Button {...action} key={index} />
))}
</div>
)}
{actions && actions?.length > 0 && <ModalFooter actions={actions} />}
{footerContent && <ModalFooter>{footerContent}</ModalFooter>}
</div>
</dialog>
)
}
})
export default Modal

View File

@@ -47,7 +47,7 @@ const Rating: React.FC<RatingProps> = ({
(hoverRating !== 0 && hoverRating - 1 >= i)
return (
<button
className="transparent-button"
className="btn-clear"
ref={(element) => {
if (starElms.current.length - 1 < i) {
starElms.current.push(element)

View File

@@ -0,0 +1,22 @@
import React from "react"
import { useInstantSearch } from "react-instantsearch"
type SearchEmptyQueryBoundaryProps = {
children: React.ReactElement
fallback: React.ReactElement
}
const SearchEmptyQueryBoundary = ({
children,
fallback,
}: SearchEmptyQueryBoundaryProps) => {
const { indexUiState } = useInstantSearch()
if (!indexUiState.query) {
return fallback
}
return children
}
export default SearchEmptyQueryBoundary

View File

@@ -0,0 +1,22 @@
import React from "react"
import clsx from "clsx"
type SearchHitGroupNameProps = {
name: string
}
const SearchHitGroupName = ({ name }: SearchHitGroupNameProps) => {
return (
<span
className={clsx(
"pb-0.25 flex px-0.5 pt-1",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark",
"text-compact-x-small-plus"
)}
>
{name}
</span>
)
}
export default SearchHitGroupName

View File

@@ -0,0 +1,190 @@
import React from "react"
import clsx from "clsx"
import { Fragment, useEffect, useMemo, useState } from "react"
import {
Configure,
ConfigureProps,
Index,
Snippet,
useHits,
useInstantSearch,
} from "react-instantsearch"
import SearchNoResult from "../NoResults"
import SearchHitGroupName from "./GroupName"
import Link from "@docusaurus/Link"
import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
type Hierarchy = "lvl0" | "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5"
export type HitType = {
hierarchy: {
lvl0: string | null
lvl1: string | null
lvl2: string | null
lvl3: string | null
lvl4: string | null
lvl5: string | null
}
_tags: string[]
url: string
type?: "lvl1" | "lvl2" | "lvl3" | "lvl4" | "lvl5" | "content"
content?: string
__position: number
__queryID?: string
objectID: string
}
type GroupedHitType = {
[k: string]: HitType[]
}
type SearchHitWrapperProps = {
configureProps: ConfigureProps
}
type IndexResults = {
[k: string]: boolean
}
const SearchHitsWrapper = ({ configureProps }: SearchHitWrapperProps) => {
const { status } = useInstantSearch()
const { algoliaConfig: algolia } = useThemeConfig() as ThemeConfig
const indices = useMemo(() => Object.values(algolia.indexNames), [])
const [hasNoResults, setHashNoResults] = useState<IndexResults>({
[indices[0]]: false,
[indices[1]]: false,
})
const showNoResults = useMemo(() => {
return Object.values(hasNoResults).every((value) => value === true)
}, [hasNoResults])
const setNoResults = (index: string, value: boolean) => {
setHashNoResults((prev: IndexResults) => ({
...prev,
[index]: value,
}))
}
return (
<div className="h-full overflow-auto">
{status !== "loading" && showNoResults && <SearchNoResult />}
{indices.map((indexName, index) => (
<Index indexName={indexName} key={index}>
<SearchHits indexName={indexName} setNoResults={setNoResults} />
<Configure {...configureProps} />
</Index>
))}
</div>
)
}
type SearchHitsProps = {
indexName: string
setNoResults: (index: string, value: boolean) => void
}
const SearchHits = ({ indexName, setNoResults }: SearchHitsProps) => {
const { hits } = useHits<HitType>()
const { status } = useInstantSearch()
// group by lvl0
const grouped = useMemo(() => {
const grouped: GroupedHitType = {}
hits.forEach((hit) => {
if (hit.hierarchy.lvl0) {
if (!grouped[hit.hierarchy.lvl0]) {
grouped[hit.hierarchy.lvl0] = []
}
grouped[hit.hierarchy.lvl0].push(hit)
}
})
return grouped
}, [hits])
useEffect(() => {
if (status !== "loading" && status !== "stalled") {
setNoResults(indexName, hits.length === 0)
}
}, [hits, status])
const getLastAvailableHeirarchy = (item: HitType) => {
return (
Object.keys(item.hierarchy)
.reverse()
.find((key) => item.hierarchy[key as Hierarchy] !== null) || ""
)
}
return (
<div className="overflow-auto">
{Object.keys(grouped).map((groupName, index) => (
<Fragment key={index}>
<SearchHitGroupName name={groupName} />
{grouped[groupName].map((item, index) => (
<div
className={clsx(
"gap-0.25 relative flex flex-1 flex-col p-0.5",
"overflow-x-hidden text-ellipsis whitespace-nowrap break-words",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"focus:bg-medusa-bg-base-hover dark:focus:bg-medusa-bg-base-hover-dark",
"focus:outline-none"
)}
key={index}
tabIndex={index}
data-hit
onClick={(e) => {
const target = e.target as Element
if (target.tagName.toLowerCase() === "div") {
target.querySelector("a")?.click()
}
}}
>
<span
className={clsx(
"text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
"max-w-full"
)}
>
<Snippet
attribute={[
"hierarchy",
item.type && item.type !== "content"
? item.type
: item.hierarchy.lvl1
? "lvl1"
: getLastAvailableHeirarchy(item),
]}
hit={item}
/>
</span>
{item.type !== "lvl1" && (
<span className="text-compact-small text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
<Snippet
attribute={
item.content
? "content"
: [
"hierarchy",
item.type || getLastAvailableHeirarchy(item),
]
}
hit={item}
/>
</span>
)}
<Link
href={item.url}
className="absolute top-0 left-0 h-full w-full"
target="_self"
/>
</div>
))}
</Fragment>
))}
</div>
)
}
export default SearchHitsWrapper

View File

@@ -0,0 +1,312 @@
import React, { useEffect, useMemo, useRef, useState } from "react"
import algoliasearch, { SearchClient } from "algoliasearch/lite"
import { InstantSearch, SearchBox } from "react-instantsearch"
import Modal from "../../Modal"
import clsx from "clsx"
import SearchEmptyQueryBoundary from "../EmptyQueryBoundary"
import SearchSuggestions from "../Suggestions"
import checkArraySameElms from "../../../utils/array-same-elms"
import SearchHitsWrapper from "../Hits"
import Button from "../../Button"
import useKeyboardShortcut from "../../../hooks/use-keyboard-shortcut"
import { OptionType } from "@medusajs/docs"
import { useSearch } from "../../../providers/Search"
import { findNextSibling, findPrevSibling } from "../../../utils/dom-utils"
import IconMagnifyingGlass from "../../../theme/Icon/MagnifyingGlass"
import IconXMark from "../../../theme/Icon/XMark"
import SelectBadge from "../../Select/Badge"
import Kbd from "../../../theme/MDXComponents/Kbd"
import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
const SearchModal = () => {
const modalRef = useRef<HTMLDialogElement | null>(null)
const { algoliaConfig: algolia } = useThemeConfig() as ThemeConfig
if (!algolia) {
throw new Error("Algolia configuration is not defined")
}
const algoliaClient = useMemo(() => {
return algoliasearch(algolia.appId, algolia.apiKey)
}, [])
const searchClient: SearchClient = useMemo(() => {
return {
...algoliaClient,
async search(requests) {
if (requests.every(({ params }) => !params?.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
})
}
return algoliaClient.search(requests)
},
}
}, [])
const options: OptionType[] = algolia.filters
const { isOpen, setIsOpen, defaultFilters } = useSearch()
const [filters, setFilters] = useState<string[]>(defaultFilters)
const formattedFilters: string = useMemo(() => {
let formatted = ""
filters.forEach((filter) => {
const split = filter.split("_")
split.forEach((f) => {
if (formatted.length) {
formatted += " OR "
}
formatted += `_tags:${f}`
})
})
return formatted
}, [filters])
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()
}
}, [isOpen])
const handleKeyAction = (e: KeyboardEvent) => {
if (!isOpen) {
return
}
e.preventDefault()
const focusedItem = modalRef.current?.querySelector(":focus") as HTMLElement
if (!focusedItem) {
// focus the first data-hit
const nextItem = modalRef.current?.querySelector(
"[data-hit]"
) as HTMLElement
nextItem?.focus()
return
}
const isHit = focusedItem.hasAttribute("data-hit")
const isInput = focusedItem.tagName.toLowerCase() === "input"
if (!isHit && !isInput) {
// ignore if focused items aren't input/data-hit
return
}
const lowerPressedKey = e.key.toLowerCase()
if (lowerPressedKey === "enter") {
if (isHit) {
// trigger click event of the focused element
focusedItem.click()
}
return
}
if (lowerPressedKey === "arrowup") {
// only hit items has action on arrow up
if (isHit) {
// find if there's a data-hit item before this one
const beforeItem = findPrevSibling(focusedItem, "[data-hit]")
if (!beforeItem) {
// focus the input
focusSearchInput()
} else {
// focus the previous item
beforeItem.focus()
}
}
} else if (lowerPressedKey === "arrowdown") {
// check if item is input or hit
if (isInput) {
// go to the first data-hit item
const nextItem = modalRef.current?.querySelector(
"[data-hit]"
) as HTMLElement
nextItem?.focus()
} else {
// handle go down for hit items
// find if there's a data-hit item after this one
const afterItem = findNextSibling(focusedItem, "[data-hit]")
if (afterItem) {
// focus the next item
afterItem.focus()
}
}
}
}
useKeyboardShortcut({
metakey: false,
shortcutKeys: ["ArrowUp", "ArrowDown", "Enter"],
action: handleKeyAction,
checkEditing: false,
preventDefault: false,
})
return (
<Modal
contentClassName={clsx(
"!p-0 overflow-hidden relative h-full",
"rounded-none md:rounded-lg flex flex-col justify-between"
)}
modalContainerClassName="w-screen h-screen !rounded-none md:!rounded-lg"
open={isOpen}
onClose={() => setIsOpen(false)}
ref={modalRef}
>
<InstantSearch
indexName={algolia.indexNames.docs}
searchClient={searchClient}
>
<div
className={clsx("bg-medusa-bg-base dark:bg-medusa-bg-base-dark flex")}
>
<SearchBox
classNames={{
root: clsx(
"h-[56px] w-full md:rounded-t-xl relative border-0 border-solid",
"border-b border-medusa-border-base dark:border-medusa-border-base-dark",
"bg-transparent"
),
form: clsx("h-full md:rounded-t-xl bg-transparent"),
input: clsx(
"w-full h-full pl-3 text-medusa-fg-base dark:text-medusa-fg-base-dark",
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
"md:rounded-t-xl text-compact-medium bg-transparent",
"appearance-none search-cancel:hidden border-0 active:outline-none focus:outline-none"
),
submit: clsx("absolute top-[18px] left-1 btn-clear p-0"),
reset: clsx(
"absolute top-0.75 right-1 hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"p-[5px] md:rounded btn-clear"
),
loadingIndicator: clsx("absolute top-[18px] right-1"),
}}
submitIconComponent={() => (
<IconMagnifyingGlass iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
)}
resetIconComponent={() => (
<IconXMark
iconColorClassName="stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
className="hidden md:block"
/>
)}
placeholder="Find something..."
autoFocus
formRef={searchBoxRef}
/>
<Button
variant="clear"
className={clsx(
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark block md:hidden",
"border-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark border-b",
"pr-1"
)}
onClick={() => setIsOpen(false)}
>
<IconXMark iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
</Button>
</div>
<div className="mx-0.5 h-[calc(100%-120px)] md:h-[332px] md:flex-initial lg:max-h-[332px] lg:min-h-[332px]">
<SearchEmptyQueryBoundary fallback={<SearchSuggestions />}>
<SearchHitsWrapper
configureProps={{
filters: formattedFilters,
attributesToSnippet: [
"content",
"hierarchy.lvl1",
"hierarchy.lvl2",
],
attributesToHighlight: [
"content",
"hierarchy.lvl1",
"hierarchy.lvl2",
],
}}
/>
</SearchEmptyQueryBoundary>
</div>
</InstantSearch>
<div
className={clsx(
"py-0.75 flex items-center justify-between px-1",
"border-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark border-t",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark"
)}
>
<SelectBadge
multiple
options={options}
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(options.map((option) => option.value))
}
}}
/>
<div className="hidden items-center gap-1 md:flex">
<div className="flex items-center gap-0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Navigation
</span>
<span className="gap-0.25 flex">
<Kbd></Kbd>
<Kbd></Kbd>
</span>
</div>
<div className="flex items-center gap-0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Open Result
</span>
<Kbd></Kbd>
</div>
</div>
</div>
</Modal>
)
}
export default SearchModal

View File

@@ -0,0 +1,78 @@
import React from "react"
import clsx from "clsx"
import InputText from "../../Input/Text"
import { MouseEvent, useMemo } from "react"
import { useSearch } from "../../../providers/Search"
import { useWindowSize } from "@docusaurus/theme-common"
import Button from "../../Button"
import IconMagnifyingGlass from "../../../theme/Icon/MagnifyingGlass"
import Kbd from "../../../theme/MDXComponents/Kbd"
import useKeyboardShortcut from "../../../hooks/use-keyboard-shortcut"
const SearchModalOpener = () => {
const { setIsOpen } = useSearch()
const windowSize = useWindowSize()
const isApple = useMemo(() => {
return typeof navigator !== "undefined"
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
: true
}, [])
useKeyboardShortcut({
shortcutKeys: ["k"],
action: () => setIsOpen((prev) => !prev),
})
const handleOpen = (
e:
| MouseEvent<HTMLDivElement, globalThis.MouseEvent>
| MouseEvent<HTMLInputElement, globalThis.MouseEvent>
| MouseEvent<HTMLButtonElement, globalThis.MouseEvent>
) => {
e.preventDefault()
if ("blur" in e.target && typeof e.target.blur === "function") {
e.target.blur()
}
setIsOpen(true)
}
return (
<>
{windowSize !== "desktop" && (
<Button variant="clear" onClick={handleOpen}>
<IconMagnifyingGlass iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
</Button>
)}
{windowSize === "desktop" && (
<div
className={clsx("relative w-min hover:cursor-pointer")}
onClick={handleOpen}
>
<IconMagnifyingGlass
iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark"
className={clsx("absolute left-0.5 top-[5px]")}
/>
<InputText
type="search"
className={clsx(
"placeholder:text-compact-small",
"!py-[5px] !pl-[36px] !pr-[8px]",
"cursor-pointer select-none"
)}
placeholder="Find something"
onClick={handleOpen}
onFocus={(e) => e.target.blur()}
tabIndex={-1}
/>
<span
className={clsx("gap-0.25 flex", "absolute right-0.5 top-[5px]")}
>
<Kbd>{isApple ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd>
</span>
</div>
)}
</>
)
}
export default SearchModalOpener

View File

@@ -0,0 +1,15 @@
import React from "react"
import IconExclamationCircleSolid from "../../../theme/Icon/ExclamationCircleSolid"
const SearchNoResult = () => {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-1">
<IconExclamationCircleSolid iconColorClassName="fill-medusa-fg-muted dark:fill-medusa-fg-muted-dark" />
<span className="text-compact-small text-medusa-fg-muted dark:text-medusa-fg-muted-dark">
No results found. Try changing selected filters.
</span>
</div>
)
}
export default SearchNoResult

View File

@@ -0,0 +1,69 @@
import React from "react"
import clsx from "clsx"
import { useInstantSearch } from "react-instantsearch"
import SearchHitGroupName from "../Hits/GroupName"
const SearchSuggestions = () => {
const { setIndexUiState } = useInstantSearch()
const suggestions = [
{
title: "Getting started? Try one of the following terms.",
items: [
"Install Medusa with create-medusa-app",
"Next.js quickstart",
"Admin dashboard quickstart",
"Commerce modules",
"Medusa architecture",
],
},
{
title: "Developing with Medusa",
items: [
"Recipes",
"How to create endpoints",
"How to create an entity",
"How to create a plugin",
"How to create an admin widget",
],
},
]
return (
<div className="h-full overflow-auto">
{suggestions.map((suggestion, index) => (
<React.Fragment key={index}>
<SearchHitGroupName name={suggestion.title} />
{suggestion.items.map((item, itemIndex) => (
<div
className={clsx(
"flex items-center justify-between",
"cursor-pointer rounded-sm p-0.5",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"focus:bg-medusa-bg-base-hover dark:focus:bg-medusa-bg-base-hover-dark",
"focus:outline-none"
)}
onClick={() =>
setIndexUiState({
query: item,
})
}
key={itemIndex}
tabIndex={itemIndex}
data-hit
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-small"
)}
>
{item}
</span>
</div>
))}
</React.Fragment>
))}
</div>
)
}
export default SearchSuggestions

View File

@@ -0,0 +1,136 @@
import React from "react"
import { useCallback, useRef, useState } from "react"
import useSelect from "../../../hooks/use-select"
import clsx from "clsx"
import SelectDropdown from "../Dropdown"
import { SelectProps } from "../types"
const SelectBadge = ({
value,
options,
setSelected,
addSelected,
removeSelected,
multiple,
className,
addAll = multiple,
handleAddAll,
...props
}: SelectProps) => {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const { isValueSelected, isAllSelected, handleChange, handleSelectAll } =
useSelect({
value,
options,
multiple,
setSelected,
removeSelected,
addSelected,
handleAddAll,
})
const getSelectedText = useCallback(() => {
let str = ""
const selectedOptions = options.filter((option) =>
value.includes(option.value)
)
if (isAllSelected) {
str = "All Areas"
} else {
if (
(!Array.isArray(value) && !value) ||
(Array.isArray(value) && !value.length)
) {
str = "None selected"
} else {
str = selectedOptions[0].label
}
}
return (
<>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-x-small-plus",
"inline-block max-w-[60px] overflow-hidden text-ellipsis"
)}
>
{str}
</span>
{!isAllSelected && selectedOptions.length > 1 && (
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
{" "}
+ {selectedOptions.length}
</span>
)}
</>
)
}, [isAllSelected, options, value])
return (
<div className={clsx("relative", className)}>
<div
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-sm border border-solid",
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-subtle-hover-dark",
"py-0.25 h-fit cursor-pointer px-0.5",
"flex items-center gap-[6px] whitespace-nowrap",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
!open && "bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark",
open &&
"bg-medusa-bg-subtle-hover dark:bg-medusa-bg-subtle-hover-dark",
className
)}
ref={ref}
onClick={(e) => {
if (!dropdownRef.current?.contains(e.target as Element)) {
setOpen((prev) => !prev)
}
}}
>
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Show results from:{" "}
</span>
{getSelectedText()}
</div>
<input
type="hidden"
name={props.name}
value={Array.isArray(value) ? value.join(",") : value}
/>
<SelectDropdown
options={options}
open={open}
setOpen={setOpen}
addAll={addAll}
multiple={multiple}
isAllSelected={isAllSelected}
isValueSelected={isValueSelected}
handleSelectAll={handleSelectAll}
handleChange={handleChange}
parentRef={ref}
ref={dropdownRef}
className={clsx(
"!top-[unset] !bottom-full",
open && "!-translate-y-0.5"
)}
/>
</div>
)
}
export default SelectBadge

View File

@@ -0,0 +1,152 @@
import React from "react"
import clsx from "clsx"
import { OptionType } from "../../../hooks/use-select"
import { forwardRef, useCallback, useEffect, useRef } from "react"
import IconCheckMini from "../../../theme/Icon/CheckMini"
import IconEllipseMiniSolid from "../../../theme/Icon/EllipseMiniSolid"
type SelectDropdownProps = {
options: OptionType[]
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
addAll?: boolean
multiple?: boolean
isAllSelected: boolean
isValueSelected: (val: string) => boolean
handleSelectAll: () => void
handleChange?: (selectedValue: string, wasSelected: boolean) => void
parentRef?: React.RefObject<HTMLDivElement>
className?: string
}
const SelectDropdown = forwardRef<HTMLDivElement, SelectDropdownProps>(
function SelectDropdown(
{
open,
setOpen,
options,
addAll,
multiple = false,
isAllSelected,
isValueSelected,
handleSelectAll,
handleChange: handleSelectChange,
parentRef,
className,
},
passedRef
) {
const ref = useRef<HTMLDivElement | null>(null)
const setRefs = useCallback(
(node: HTMLDivElement) => {
// Ref's from useRef needs to have the node assigned to `current`
ref.current = node
if (typeof passedRef === "function") {
passedRef(node)
} else if (passedRef && "current" in passedRef) {
passedRef.current = node
}
},
[passedRef]
)
const handleChange = (clickedValue: string, wasSelected: boolean) => {
handleSelectChange?.(clickedValue, wasSelected)
if (!multiple) {
setOpen(false)
}
}
const handleOutsideClick = useCallback(
(e: MouseEvent) => {
if (
open &&
!ref.current?.contains(e.target as Element) &&
!parentRef?.current?.contains(e.target as Element)
) {
setOpen(false)
}
},
[open, parentRef, setOpen]
)
useEffect(() => {
document.body.addEventListener("click", handleOutsideClick)
return () => {
document.body.removeEventListener("click", handleOutsideClick)
}
}, [handleOutsideClick])
const getSelectOption = (option: OptionType, index: number) => {
const isSelected = option.isAllOption
? isAllSelected
: isValueSelected(option.value)
return (
<li
key={index}
className={clsx(
"pr-0.75 relative rounded-sm py-0.5 pl-2.5",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"[&>svg]:left-0.75 cursor-pointer [&>svg]:absolute [&>svg]:top-0.5",
!isSelected && "text-compact-small",
isSelected && "text-compact-small-plus"
)}
onClick={() => {
if (option.isAllOption) {
handleSelectAll()
} else {
handleChange(option.value, isSelected)
}
}}
>
{isSelected && (
<>
{multiple && (
<IconCheckMini className="stroke-medusa-fg-base dark:stroke-medusa-fg-base-dark" />
)}
{!multiple && (
<IconEllipseMiniSolid className="fill-medusa-fg-base dark:fill-medusa-fg-base-dark" />
)}
</>
)}
{option.label}
</li>
)
}
return (
<div
className={clsx(
"absolute top-full left-0 w-full",
"z-10 h-0 translate-y-0 overflow-hidden transition-transform",
open && "h-auto translate-y-0.5 !overflow-visible",
className
)}
ref={setRefs}
>
<ul
className={clsx(
"p-0.25 mb-0 w-full overflow-auto rounded",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark text-medusa-fg-base dark:text-medusa-fg-base-dark",
"shadow-flyout dark:shadow-flyout-dark list-none"
)}
>
{addAll &&
getSelectOption(
{
value: "all",
label: "All Areas",
isAllOption: true,
},
-1
)}
{options.map(getSelectOption)}
</ul>
</div>
)
}
)
export default SelectDropdown

View File

@@ -0,0 +1,128 @@
import React from "react"
import clsx from "clsx"
import { useRef, useState } from "react"
import Badge from "../../Badge"
import useSelect from "../../../hooks/use-select"
import SelectDropdown from "../Dropdown"
import { SelectProps } from "../types"
import IconChevronUpDown from "../../../theme/Icon/ChevronUpDown"
import IconXMarkMini from "../../../theme/Icon/XMarkMini"
const SelectInput = ({
value,
options,
setSelected,
addSelected,
removeSelected,
multiple,
className,
addAll = multiple,
handleAddAll,
showClearButton = true,
...props
}: SelectProps) => {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const {
isValueSelected,
hasSelectedValue,
hasSelectedValues,
selectedValues,
isAllSelected,
handleChange,
handleSelectAll,
} = useSelect({
value,
options,
multiple,
setSelected,
removeSelected,
addSelected,
handleAddAll,
})
return (
<div
className={clsx(
"px-0.75 relative py-[9px]",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-sm border border-solid",
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-neutral dark:shadow-button-neutral-dark",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"active:shadow-active dark:active:shadow-active-dark",
"focus:shadow-active dark:focus:shadow-active-dark",
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:text-medusa-fg-disabled dark:disabled:text-medusa-fg-disabled-dark",
"flex items-center gap-0.5",
!hasSelectedValues &&
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
hasSelectedValues &&
"placeholder:text-medusa-fg-base dark:placeholder:text-medusa-fg-base-dark",
className
)}
ref={ref}
onClick={(e) => {
if (!dropdownRef.current?.contains(e.target as Element)) {
setOpen((prev) => !prev)
}
}}
>
{hasSelectedValues && (
<Badge
variant="neutral"
className={clsx("flex", showClearButton && "flex-1")}
>
<span
className={clsx(
"text-compact-medium-plus inline-block",
showClearButton && "mr-0.125"
)}
>
{(value as string[]).length}
</span>
{showClearButton && (
<IconXMarkMini
iconColorClassName="stroke-medusa-tag-neutral-icon dark:stroke-medusa-tag-neutral-icon-dark"
onClick={(e) => {
e.stopPropagation()
setSelected?.([])
}}
/>
)}
</Badge>
)}
<span
className={clsx(
"inline-block flex-1 select-none overflow-ellipsis whitespace-nowrap break-words",
hasSelectedValues && "max-w-1/3"
)}
>
{!multiple && hasSelectedValue && selectedValues.length
? selectedValues[0].label
: props.placeholder}
</span>
<IconChevronUpDown iconColorClassName="stroke-medusa-fg-muted dark:stroke-medusa-fg-muted-dark" />
<input
type="hidden"
name={props.name}
value={Array.isArray(value) ? value.join(",") : value}
/>
<SelectDropdown
options={options}
open={open}
setOpen={setOpen}
addAll={addAll}
multiple={multiple}
isAllSelected={isAllSelected}
isValueSelected={isValueSelected}
handleSelectAll={handleSelectAll}
handleChange={handleChange}
parentRef={ref}
ref={dropdownRef}
/>
</div>
)
}
export default SelectInput

View File

@@ -0,0 +1,9 @@
import { OptionType, SelectOptions } from "../../hooks/use-select"
export type SelectProps = {
options: OptionType[]
multiple?: boolean
addAll?: boolean
showClearButton?: boolean
} & SelectOptions &
React.ComponentProps<"input">

View File

@@ -1,33 +0,0 @@
import React from "react"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import Head from "@docusaurus/Head"
type StructuredDataSearchboxProps = React.HTMLAttributes<HTMLScriptElement>
const StructuredDataSearchbox: React.FC<StructuredDataSearchboxProps> = () => {
const {
siteConfig: { url },
} = useDocusaurusContext()
return (
<Head>
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
url,
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: `${url}/search?q={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
})}
</script>
</Head>
)
}
export default StructuredDataSearchbox

View File

@@ -14,7 +14,7 @@ const TextArea: React.FC<TextAreaProps> = (props) => {
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"rounded border-medusa-border-loud-muted dark:border-medusa-border-loud-muted-dark border border-solid",
"rounded border-medusa-border-base dark:border-medusa-border-base-dark border border-solid",
"px-0.75 pt-0.4 pb-[9px]",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"focus:border-medusa-border-interactive dark:focus:border-medusa-border-interactive-dark",

View File

@@ -1,197 +0,0 @@
.DocSearch-Modal {
@apply border border-solid !border-medusa-border-base dark:!border-medusa-border-base-dark;
@apply lg:!rounded !relative;
@apply md:!m-[unset] md:w-[560px];
}
.DocSearch-SearchBar {
@apply !p-0;
}
.DocSearch-Form {
--docsearch-spacing: theme(margin[1.5]);
--docsearch-searchbox-height: 56px;
--docsearch-searchbox-focus-background: var(--docsearch-modal-background) !important;
@apply !rounded-t !rounded-b-none border-0 border-b border-solid !border-medusa-border-base dark:!border-medusa-border-base-dark;
}
.DocSearch-LoadingIndicator svg, .DocSearch-MagnifierLabel svg {
@apply !w-[20px] !h-[20px];
}
.DocSearch-Input {
@apply !text-medium !pl-1;
@apply placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark;
}
.DocSearch-Dropdown {
@apply !pt-0 !pb-2.5 !px-0;
@apply !max-h-[416px];
}
.DocSearch-Hit-source {
@apply !m-0 !text-compact-small-plus uppercase text-medusa-fg-muted dark:text-medusa-fg-muted-dark;
@apply border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
@apply !py-[10px] !px-1.5;
}
.DocSearch-Footer {
@apply !hidden;
}
.DocSearch-Hit {
@apply !p-0;
}
.DocSearch-Hit:not(.DocSearch-Hit--Child) .DocSearch-Hit-icon {
@apply !w-2.5 !h-2.5 p-0.125 border border-solid border-medusa-border-strong dark:border-medusa-border-strong-dark;
@apply rounded flex justify-center items-center;
@apply before:content-[''] before:w-2 before:h-2 before:bg-no-repeat before:bg-center before:bg-contain before:bg-search-hit dark:before:bg-search-hit-dark;
}
.DocSearch-Hit-icon svg {
@apply hidden;
}
.DocSearch-Hit--Child .DocSearch-Hit-icon {
@apply hidden;
}
.DocSearch-Hit a {
@apply !py-0.75 !px-1.5 !shadow-none !rounded-none;
}
.DocSearch-Hit:not(:last-of-type) a {
@apply border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
}
.DocSearch-Hit-content-wrapper {
@apply !mt-0 !mx-1;
}
.DocSearch-Hit-title,
.DocSearch-Hit-title mark {
@apply !text-medusa-fg-base dark:!text-medusa-fg-base-dark;
@apply !text-compact-small-plus;
}
.DocSearch-Hit-path {
@apply !text-medusa-fg-subtle dark:!text-medusa-fg-subtle-dark !text-compact-small;
}
.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path {
--docsearch-hit-active-color: theme(colors.medusa.fg.subtle.DEFAULT);
}
html[data-theme="dark"] .DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path {
--docsearch-hit-active-color: theme(colors.medusa.fg.subtle.dark);
}
.DocSearch-Hit[aria-selected=true] a {
@apply !bg-medusa-bg-base-hover dark:!bg-medusa-bg-base-hover-dark;
}
.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-content-wrapper + .DocSearch-Hit-action:last-child {
@apply h-1.5 w-1.5 border border-solid border-medusa-border-strong dark:border-medusa-border-strong-dark;
@apply rounded bg-medusa-bg-base dark:bg-medusa-bg-base-dark p-0.125 flex justify-center items-center;
@apply before:content-[''] before:w-[20px] before:h-[20px] before:bg-search-arrow dark:before:bg-search-arrow-dark before:bg-no-repeat before:bg-center;
}
.DocSearch-Hit-content-wrapper + .DocSearch-Hit-action:last-child svg,
.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-content-wrapper + .DocSearch-Hit-action:last-child svg {
@apply hidden;
}
.DocSearch-Hit[aria-selected=true] mark {
@apply !no-underline;
}
.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree {
@apply text-medusa-border-strong dark:text-medusa-border-strong-dark;
}
.DocSearch-Hit-Tree {
@apply text-medusa-border-base dark:text-medusa-border-base-dark;
}
.DocSearch-HitsFooter {
@apply z-[100] absolute bottom-0 left-0 w-full h-2.5 !bg-medusa-bg-base dark:!bg-medusa-bg-base-dark;
@apply border-0 border-t border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
}
.DocSearch-HitsFooter a {
@apply !border-b-0;
}
.DocSearch-Reset {
@apply !rounded transition-all duration-200 ease-ease;
@apply hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark;
}
.DocSearch-NoResults .DocSearch-Screen-Icon {
@apply w-[20px] h-[20px] !p-0 flex justify-center items-center mt-0 mb-1 mx-auto;
@apply before:content-[''] before:w-full before:h-full before:bg-no-repeat before:bg-center before:bg-contain;
@apply before:bg-search-no-result dark:before:bg-search-no-result-dark;
}
.DocSearch-NoResults .DocSearch-Screen-Icon svg {
@apply hidden;
}
.DocSearch-NoResults .DocSearch-Title {
@apply !text-compact-small !pl-1;
}
.DocSearch-NoResults-Prefill-List {
@apply text-center !text-compact-small-plus !pl-1;
}
.DocSearch-NoResults-Prefill-List li::marker {
@apply content-[''];
}
.DocSearch-NoResults-Prefill-List li button {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
}
.DocSearch-Button {
@apply w-full !h-full !rounded lg:!border lg:!border-solid lg:!border-medusa-border-base lg:dark:!border-medusa-border-base-dark border-0;
@apply lg:!bg-medusa-bg-field lg:dark:!bg-medusa-bg-field-dark !bg-transparent;
@apply lg:hover:!bg-medusa-bg-field-hover lg:dark:hover:!bg-medusa-bg-field-hover-dark;
@apply !py-[6px] !pl-0.5 relative;
@apply hover:!border-medusa-border-base hover:dark:!border-medusa-border-base-dark;
@apply active:!border-medusa-border-base active:dark:!border-medusa-border-base-dark;
@apply focus:!border-medusa-border-base focus:dark:!border-medusa-border-base-dark;
}
.DocSearch-Container {
@apply !z-[1001] md:flex md:justify-center md:items-center;
}
.DocSearch-Button .DocSearch-Search-Icon {
@apply invisible;
}
.DocSearch-Button-Container {
@apply before:content-[''] before:h-[20px] before:w-[20px] before:absolute before:left-0.5 before:top-[5px];
@apply before:bg-magnifying-glass dark:before:bg-magnifying-glass-dark before:bg-no-repeat;
}
.DocSearch-Button-Placeholder {
@apply text-medusa-fg-muted dark:text-medusa-fg-muted-dark;
@apply !pl-0.5 !text-compact-small lg:!block !hidden;
}
.DocSearch-Button-Keys {
@apply w-fit !min-w-[unset] lg:!flex !hidden;
}
.DocSearch-Button-Key {
@apply !shadow-none !rounded-md !text-compact-x-small-plus !font-base align-middle !p-0.125;
@apply !border !border-solid !border-medusa-tag-neutral-border dark:!border-medusa-tag-neutral-border-dark;
@apply [&span]:hidden [&:not(:last-child)]:!mr-0.25 last:!mr-0;
}
[class*=searchBox] {
@apply lg:w-[280px] lg:max-w-[280px] lg:!h-2 lg:!p-0 !static;
}

View File

@@ -2,6 +2,17 @@
@tailwind components;
@tailwind utilities;
@layer base {
body[data-modal="opened"] {
@apply !overflow-hidden;
}
mark {
@apply bg-medusa-bg-highlight dark:bg-medusa-bg-highlight-dark;
@apply text-medusa-fg-interactive dark:text-medusa-fg-interactive-dark;
}
}
@layer components {
.sidebar-title {
@apply !m-0 !py-1 !px-0;
@@ -116,13 +127,14 @@
@apply inline-flex flex-row justify-center items-center;
@apply py-[5px] px-0.75 rounded-sm cursor-pointer;
@apply bg-button-neutral bg-medusa-button-neutral dark:bg-button-neutral-dark dark:bg-medusa-button-neutral-dark;
@apply hover:bg-medusa-button-neutral-hover hover:bg-button-neutral-hover dark:hover:bg-medusa-button-neutral-hover-dark dark:hover:bg-button-neutral-hover-dark hover:no-underline;
@apply active:bg-medusa-button-neutral-pressed active:bg-button-neutral-pressed dark:active:bg-medusa-button-neutral-pressed-dark dark:active:bg-button-neutral-pressed-dark;
@apply focus:bg-medusa-button-neutral-pressed focus:bg-button-neutral-pressed dark:focus:bg-medusa-button-neutral-pressed-dark dark:focus:bg-button-neutral-pressed-dark;
@apply hover:bg-medusa-button-neutral-hover hover:bg-no-image dark:hover:bg-medusa-button-neutral-hover-dark hover:no-underline;
@apply active:bg-medusa-button-neutral-pressed active:bg-no-image dark:active:bg-medusa-button-neutral-pressed-dark;
@apply focus:bg-medusa-button-neutral-pressed focus:bg-no-image dark:focus:bg-medusa-button-neutral-pressed-dark;
@apply disabled:!bg-no-image disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark;
@apply disabled:cursor-not-allowed;
@apply border border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
@apply text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark;
@apply hover:text-medusa-fg-base hover:dark:text-medusa-fg-base-dark;
@apply shadow-button-neutral focus:shadow-button-neutral-focused active:shadow-button-neutral-focused transition-shadow;
@apply dark:shadow-button-neutral dark:focus:shadow-button-neutral-focused dark:active:shadow-button-neutral-focused;
@apply select-none;
@@ -132,9 +144,9 @@
@apply inline-flex flex-row justify-center items-center;
@apply py-[5px] px-0.75 rounded-sm cursor-pointer;
@apply bg-button-inverted bg-medusa-button-inverted dark:bg-button-inverted-dark dark:bg-medusa-button-inverted-dark;
@apply hover:bg-medusa-button-inverted-hover hover:bg-button-inverted-hover dark:hover:bg-medusa-button-inverted-hover-dark dark:hover:bg-button-inverted-hover-dark hover:no-underline;
@apply active:bg-medusa-button-inverted-pressed active:bg-button-inverted-pressed dark:active:bg-medusa-button-inverted-pressed-dark dark:active:bg-button-inverted-pressed-dark;
@apply focus:bg-medusa-button-inverted-pressed focus:bg-button-inverted-pressed dark:focus:bg-medusa-button-inverted-pressed-dark dark:focus:bg-button-inverted-pressed-dark;
@apply hover:bg-medusa-button-inverted-hover hover:bg-no-image dark:hover:bg-medusa-button-inverted-hover-dark hover:no-underline;
@apply active:bg-medusa-button-inverted-pressed active:bg-no-image dark:active:bg-medusa-button-inverted-pressed-dark;
@apply focus:bg-medusa-button-inverted-pressed focus:bg-no-image dark:focus:bg-medusa-button-inverted-pressed-dark;
@apply shadow-button-colored active:shadow-button-colored-focused focus:shadow-button-colored-focused transition-shadow;
@apply dark:shadow-button-colored-dark dark:active:shadow-button-colored-focused-dark dark:focus:shadow-button-colored-focused-dark;
@apply disabled:!bg-no-image disabled:bg-medusa-button-disabled dark:disabled:bg-medusa-button-disabled-dark;
@@ -146,15 +158,16 @@
}
.navbar-action-icon-item {
@apply lg:bg-docs-button-neutral lg:dark:bg-docs-button-neutral-dark hover:!bg-no-image lg:active:!bg-no-image hover:bg-medusa-button-neutral-hover dark:hover:bg-medusa-button-neutral-hover-dark;
@apply lg:active:bg-medusa-button-neutral-pressed lg:dark:active:bg-medusa-button-neutral-pressed-dark;
@apply lg:focus:shadow-button-secondary-focus lg:dark:focus:shadow-button-secondary-focus-dark;
@apply lg:border lg:border-solid lg:border-medusa-border-loud-muted lg:dark:border-medusa-border-loud-muted-dark rounded;
@apply lg:bg-button-neutral lg:bg-medusa-button-neutral lg:dark:bg-button-neutral-dark lg:dark:bg-medusa-button-neutral-dark;
@apply lg:hover:bg-medusa-button-neutral-hover lg:hover:bg-no-image lg:dark:hover:bg-medusa-button-neutral-hover-dark lg:hover:no-underline;
@apply lg:active:bg-medusa-button-neutral-pressed lg:active:bg-no-image lg:dark:active:bg-medusa-button-neutral-pressed-dark;
@apply lg:focus:bg-medusa-button-neutral-pressed lg:focus:bg-no-image lg:dark:focus:bg-medusa-button-neutral-pressed-dark;
@apply lg:lg:border lg:border-solid lg:border-medusa-border-base lg:dark:border-medusa-border-base-dark rounded;
@apply w-2 h-2 flex justify-center items-center cursor-pointer;
}
.transparent-button {
@apply bg-transparent border-0 text-inherit cursor-pointer p-0;
.btn-clear {
@apply bg-transparent shadow-none border-0 outline-none cursor-pointer;
}
}
@@ -167,7 +180,6 @@
@import url('./_variables.css');
@import url('./_docusaurus.css');
@import url('./components/docsearch.css');
@import url('./components/sidebar.css');
@import url('./components/toc.css');
@import url('./components/tooltip.css');

View File

@@ -0,0 +1,64 @@
import { useCallback, useEffect } from "react"
type useKeyboardShortcutOptions = {
metakey?: boolean
shortcutKeys: string[]
action: (e: KeyboardEvent) => void
checkEditing?: boolean
preventDefault?: boolean
}
const useKeyboardShortcut = ({
metakey = true,
shortcutKeys,
action,
checkEditing = true,
preventDefault = true,
}: useKeyboardShortcutOptions) => {
function isEditingContent(event: KeyboardEvent) {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === "INPUT" ||
tagName === "SELECT" ||
tagName === "TEXTAREA"
)
}
const checkKeysPressed = useCallback(
(pressedKey: string) => {
const lowerPressedKey = pressedKey.toLowerCase()
return shortcutKeys.some(
(value) => lowerPressedKey === value.toLowerCase()
)
},
[shortcutKeys]
)
const sidebarShortcut = useCallback(
(e: KeyboardEvent) => {
if (
(!metakey || e.metaKey || e.ctrlKey) &&
checkKeysPressed(e.key) &&
(!checkEditing || !isEditingContent(e))
) {
if (preventDefault) {
e.preventDefault()
}
action(e)
}
},
[metakey, checkKeysPressed, checkEditing, action, preventDefault]
)
useEffect(() => {
window.addEventListener("keydown", sidebarShortcut)
return () => {
window.removeEventListener("keydown", sidebarShortcut)
}
}, [sidebarShortcut])
}
export default useKeyboardShortcut

View File

@@ -0,0 +1,93 @@
import { useCallback, useMemo } from "react"
export type OptionType = {
value: string
label: string
index?: string
isAllOption?: boolean
}
export type SelectOptions = {
value: string | string[]
multiple?: boolean
options: OptionType[]
setSelected?: (value: string | string[]) => void
addSelected?: (value: string) => void
removeSelected?: (value: string) => void
handleAddAll?: (isAllSelected: boolean) => void
}
const useSelect = ({
value,
options,
multiple = false,
setSelected,
addSelected,
removeSelected,
handleAddAll,
}: SelectOptions) => {
const isValueSelected = useCallback(
(val: string) => {
return (
(typeof value === "string" && val === value) ||
(Array.isArray(value) && value.includes(val))
)
},
[value]
)
// checks if there are multiple selected values
const hasSelectedValues = useMemo(() => {
return multiple && Array.isArray(value) && value.length > 0
}, [value, multiple])
// checks if there are any selected values,
// whether multiple or one
const hasSelectedValue = useMemo(() => {
return hasSelectedValues || (typeof value === "string" && value.length)
}, [hasSelectedValues, value])
const selectedValues: OptionType[] = useMemo(() => {
if (typeof value === "string") {
const selectedValue = options.find((option) => option.value === value)
return selectedValue ? [selectedValue] : []
} else if (Array.isArray(value)) {
return options.filter((option) => value.includes(option.value))
}
return []
}, [options, value])
const isAllSelected = useMemo(() => {
return Array.isArray(value) && value.length === options.length
}, [options, value])
const handleChange = (selectedValue: string, wasSelected: boolean) => {
if (multiple) {
wasSelected
? removeSelected?.(selectedValue)
: addSelected?.(selectedValue)
} else {
setSelected?.(selectedValue)
}
}
const handleSelectAll = () => {
if (handleAddAll) {
handleAddAll(isAllSelected)
} else {
setSelected?.(options.map((option) => option.value))
}
}
return {
isValueSelected,
hasSelectedValue,
hasSelectedValues,
selectedValues,
isAllSelected,
handleChange,
handleSelectAll,
}
}
export default useSelect

View File

@@ -5,6 +5,7 @@ import Modal, { ModalProps } from "../../components/Modal"
type ModalContextType = {
modalProps: ModalProps | null
setModalProps: (value: ModalProps | null) => void
closeModal: () => void
}
const ModalContext = createContext<ModalContextType | null>(null)
@@ -13,10 +14,10 @@ type ModalProviderProps = {
children?: React.ReactNode
}
const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
const ModalProvider = ({ children }: ModalProviderProps) => {
const [modalProps, setModalProps] = useState<ModalProps | null>(null)
const handleClose = () => {
const closeModal = () => {
setModalProps(null)
}
@@ -25,10 +26,16 @@ const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
value={{
modalProps,
setModalProps,
closeModal,
}}
>
{children}
{modalProps && <Modal {...modalProps} onClose={handleClose} />}
{modalProps && (
<>
<div className="bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark fixed top-0 left-0 z-[499] h-screen w-screen"></div>
<Modal {...modalProps} onClose={closeModal} />
</>
)}
</ModalContext.Provider>
)
}

View File

@@ -0,0 +1,68 @@
import React, { useEffect } from "react"
import { createContext, useContext, useState } from "react"
import SearchModal from "../../components/Search/Modal"
import { useLocalPathname } from "@docusaurus/theme-common/internal"
import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
import checkArraySameElms from "../../utils/array-same-elms"
type SearchContextType = {
isOpen: boolean
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>
defaultFilters: string[]
setDefaultFilters: (value: string[]) => void
}
const SearchContext = createContext<SearchContextType | null>(null)
type SearchProviderProps = {
children: React.ReactNode
}
const SearchProvider = ({ children }: SearchProviderProps) => {
const [isOpen, setIsOpen] = useState(false)
const [defaultFilters, setDefaultFilters] = useState<string[]>([])
const currentPath = useLocalPathname()
const { algoliaConfig: algolia } = useThemeConfig() as ThemeConfig
useEffect(() => {
let resultFilters = []
algolia.defaultFiltersByPath.some((filtersByPath) => {
if (currentPath.startsWith(filtersByPath.path)) {
resultFilters = filtersByPath.filters
}
})
if (!resultFilters.length && algolia.defaultFilters) {
resultFilters = algolia.defaultFilters
}
if (!checkArraySameElms(defaultFilters, resultFilters)) {
setDefaultFilters(resultFilters)
}
}, [currentPath])
return (
<SearchContext.Provider
value={{
isOpen,
setIsOpen,
defaultFilters,
setDefaultFilters,
}}
>
{children}
<SearchModal />
</SearchContext.Provider>
)
}
export default SearchProvider
export const useSearch = (): SearchContextType => {
const context = useContext(SearchContext)
if (!context) {
throw new Error("useSearch must be used inside a SearchProvider")
}
return context
}

View File

@@ -20,6 +20,7 @@ import SidebarProvider from "@site/src/providers/Sidebar"
import NotificationProvider from "@site/src/providers/Notification"
import UserProvider from "@site/src/providers/User"
import ModalProvider from "../../providers/Modal"
import SearchProvider from "../../providers/Search"
function DocPageMetadata(props: Props): JSX.Element {
const { versionMetadata } = props
@@ -67,9 +68,7 @@ export default function DocPage(props: Props): JSX.Element {
<LearningPathProvider>
<SidebarProvider sidebarName={sidebarName}>
<NotificationProvider>
<ModalProvider>
<DocPageLayout>{docElement}</DocPageLayout>
</ModalProvider>
<DocPageLayout>{docElement}</DocPageLayout>
</NotificationProvider>
</SidebarProvider>
</LearningPathProvider>

View File

@@ -0,0 +1,41 @@
import React from "react"
import { IconProps } from ".."
const IconArrowDownLeftMini: React.FC<IconProps> = ({
iconColorClassName,
...props
}) => {
return (
<svg
width={props.width || 20}
height={props.height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8.00002 8.66675L4.66669 12.0001L8.00002 15.3334"
className={
iconColorClassName ||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.3334 4.66675V9.33341C15.3334 10.0407 15.0524 10.7189 14.5523 11.219C14.0522 11.7191 13.3739 12.0001 12.6667 12.0001H4.66669"
className={
iconColorClassName ||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default IconArrowDownLeftMini

View File

@@ -0,0 +1,31 @@
import React from "react"
import { IconProps } from ".."
const IconCheckMini: React.FC<IconProps> = ({
iconColorClassName,
...props
}) => {
return (
<svg
width={props.width || 20}
height={props.height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5.83334 10.4167L9.16668 13.75L14.1667 6.25"
className={
iconColorClassName ||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default IconCheckMini

View File

@@ -0,0 +1,31 @@
import React from "react"
import { IconProps } from ".."
const IconChevronUpDown: React.FC<IconProps> = ({
iconColorClassName,
...props
}) => {
return (
<svg
width={props.width || 20}
height={props.height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6 12.75L9.75 16.5L13.5 12.75M6 6.75L9.75 3L13.5 6.75"
className={
iconColorClassName ||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default IconChevronUpDown

View File

@@ -0,0 +1,30 @@
import React from "react"
import { IconProps } from ".."
const IconEllipseMiniSolid: React.FC<IconProps> = ({
iconColorClassName,
...props
}) => {
return (
<svg
width={props.width || 20}
height={props.height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<circle
cx="10"
cy="10"
r="2"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
</svg>
)
}
export default IconEllipseMiniSolid

View File

@@ -0,0 +1,31 @@
import React from "react"
import { IconProps } from ".."
const IconMagnifyingGlass: React.FC<IconProps> = ({
iconColorClassName,
...props
}) => {
return (
<svg
width={props.width || 20}
height={props.height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M17.4999 17.8713L13.1691 13.5404M13.1691 13.5404C14.3412 12.3683 14.9997 10.7785 14.9997 9.12084C14.9997 7.46317 14.3412 5.8734 13.1691 4.70126C11.9969 3.52911 10.4072 2.87061 8.7495 2.87061C7.09184 2.87061 5.50207 3.52911 4.32992 4.70126C3.15777 5.8734 2.49927 7.46317 2.49927 9.12084C2.49927 10.7785 3.15777 12.3683 4.32992 13.5404C5.50207 14.7126 7.09184 15.3711 8.7495 15.3711C10.4072 15.3711 11.9969 14.7126 13.1691 13.5404V13.5404Z"
className={
iconColorClassName ||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default IconMagnifyingGlass

View File

@@ -0,0 +1,31 @@
import React from "react"
import { IconProps } from ".."
const IconXMarkMini: React.FC<IconProps> = ({
iconColorClassName,
...props
}) => {
return (
<svg
width={props.width || 20}
height={props.height || 20}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6 14L14 6M6 6L14 14"
className={
iconColorClassName ||
"stroke-medusa-fg-subtle dark:stroke-medusa-fg-subtle-dark"
}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default IconXMarkMini

View File

@@ -1,6 +1,7 @@
import IconAcademicCapSolid from "./AcademicCapSolid"
import IconAdjustments from "./Adjustments"
import IconAlert from "./Alert"
import IconArrowDownLeftMini from "./ArrowDownLeftMini"
import IconArrowDownTray from "./ArrowDownTray"
import IconBackArrow from "./BackArrow"
import IconBarsThree from "./BarsThree"
@@ -19,7 +20,9 @@ import IconCashSolid from "./CashSolid"
import IconChannels from "./Channels"
import IconChannelsSolid from "./ChannelsSolid"
import IconCheckCircleSolid from "./CheckCircleSolid"
import IconCheckMini from "./CheckMini"
import IconChevronDoubleLeftMiniSolid from "./ChevronDoubleLeftMiniSolid"
import IconChevronUpDown from "./ChevronUpDown"
import IconCircleDottedLine from "./CircleDottedLine"
import IconCircleMiniSolid from "./CircleMiniSolid"
import IconCircleStack from "./CircleStack"
@@ -43,6 +46,7 @@ import IconDarkMode from "./DarkMode"
import IconDiscord from "./Discord"
import IconDocumentText from "./DocumentText"
import IconDocumentTextSolid from "./DocumentTextSolid"
import IconEllipseMiniSolid from "./EllipseMiniSolid"
import IconExclamationCircleSolid from "./ExclamationCircleSolid"
import IconExternalLink from "./ExternalLink"
import IconFlyingBox from "./FlyingBox"
@@ -61,6 +65,7 @@ import IconLightBulb from "./LightBulb"
import IconLightBulbSolid from "./LightBulbSolid"
import IconLightMode from "./LightMode"
import IconLinkedIn from "./LinkedIn"
import IconMagnifyingGlass from "./MagnifyingGlass"
import IconMap from "./Map"
import IconNewspaper from "./Newspaper"
import IconNextjs from "./Nextjs"
@@ -93,6 +98,8 @@ import IconTwitter from "./Twitter"
import IconUser from "./User"
import IconUsersSolid from "./UsersSolid"
import IconXCircleSolid from "./XCircleSolid"
import IconXMark from "./XMark"
import IconXMarkMini from "./XMarkMini"
export type IconProps = {
width?: number
@@ -105,6 +112,7 @@ export default {
"academic-cap-solid": IconAcademicCapSolid,
adjustments: IconAdjustments,
alert: IconAlert,
"arrow-down-left-mini": IconArrowDownLeftMini,
"arrow-down-tray": IconArrowDownTray,
"back-arrow": IconBackArrow,
"bars-three": IconBarsThree,
@@ -123,7 +131,9 @@ export default {
"channels-solid": IconChannelsSolid,
channels: IconChannels,
"check-circle-solid": IconCheckCircleSolid,
"check-mini": IconCheckMini,
"chevron-double-left-mini-solid": IconChevronDoubleLeftMiniSolid,
"chevron-up-down": IconChevronUpDown,
"circle-dotted-line": IconCircleDottedLine,
"circle-mini-solid": IconCircleMiniSolid,
"circle-stack": IconCircleStack,
@@ -147,6 +157,7 @@ export default {
discord: IconDiscord,
"document-text": IconDocumentText,
"document-text-solid": IconDocumentTextSolid,
"ellipse-mini-solid": IconEllipseMiniSolid,
"exclamation-circle-solid": IconExclamationCircleSolid,
"external-link": IconExternalLink,
"flying-box": IconFlyingBox,
@@ -165,6 +176,7 @@ export default {
"light-bulb-solid": IconLightBulbSolid,
"light-mode": IconLightMode,
linkedin: IconLinkedIn,
"magnifying-glass": IconMagnifyingGlass,
map: IconMap,
newspaper: IconNewspaper,
nextjs: IconNextjs,
@@ -197,4 +209,6 @@ export default {
user: IconUser,
"users-solid": IconUsersSolid,
"x-circle-solid": IconXCircleSolid,
"x-mark": IconXMark,
"x-mark-mini": IconXMarkMini,
}

View File

@@ -15,8 +15,9 @@ import type { Props } from "@theme/Layout"
import useIsBrowser from "@docusaurus/useIsBrowser"
import { useLocation } from "@docusaurus/router"
import "animate.css"
import StructuredDataSearchbox from "@site/src/components/StructuredData/Searchbox"
import { useUser } from "@site/src/providers/User"
import SearchProvider from "../../providers/Search"
import ModalProvider from "../../providers/Modal"
export default function Layout(props: Props): JSX.Element {
const {
@@ -54,24 +55,29 @@ export default function Layout(props: Props): JSX.Element {
return (
<LayoutProvider>
<PageMetadata title={title} description={description} />
{isBrowser && location.pathname === "/" && <StructuredDataSearchbox />}
<SkipToContent />
<ModalProvider>
<SearchProvider>
<PageMetadata title={title} description={description} />
<SkipToContent />
<Navbar />
<Navbar />
<div
id={SkipToContentFallbackId}
className={clsx(
ThemeClassNames.wrapper.main,
"flex-auto flex-grow flex-shrink-0",
wrapperClassName
)}
>
<ErrorBoundary fallback={(params) => <ErrorPageContent {...params} />}>
{children}
</ErrorBoundary>
</div>
<div
id={SkipToContentFallbackId}
className={clsx(
ThemeClassNames.wrapper.main,
"flex-auto flex-grow flex-shrink-0",
wrapperClassName
)}
>
<ErrorBoundary
fallback={(params) => <ErrorPageContent {...params} />}
>
{children}
</ErrorBoundary>
</div>
</SearchProvider>
</ModalProvider>
</LayoutProvider>
)
}

View File

@@ -0,0 +1,27 @@
import React from "react"
import clsx from "clsx"
type KbdProps = {
className?: string
} & React.ComponentProps<"kbd">
const Kbd: React.FC<KbdProps> = ({ children, className, ...props }) => {
return (
<kbd
className={clsx(
"h-[22px] w-[22px] rounded-sm p-0 border-solid",
"inline-flex items-center justify-center",
"border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark border",
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark",
"text-compact-x-small-plus shadow-none",
className
)}
{...props}
>
{children}
</kbd>
)
}
export default Kbd

View File

@@ -5,6 +5,7 @@ import CloudinaryImage from "@site/src/components/CloudinaryImage"
import MDXDetails from "./Details"
import MDXSummary from "./Summary"
import MDXA from "./A"
import Kbd from "./Kbd"
export default {
// Re-use the default mapping
@@ -14,4 +15,5 @@ export default {
details: MDXDetails,
summary: MDXSummary,
a: MDXA,
kbd: Kbd,
}

View File

@@ -0,0 +1,10 @@
import React from "react"
import clsx from "clsx"
import type { Props } from "@theme/Navbar/Search"
export default function NavbarSearch({
children,
className,
}: Props): JSX.Element {
return <div className={clsx("flex", className)}>{children}</div>
}

View File

@@ -0,0 +1,18 @@
import React from "react"
import NavbarSearch from "@theme/Navbar/Search"
import type { Props } from "@theme/NavbarItem/SearchNavbarItem"
import SearchModalOpener from "../../components/Search/ModalOpener"
export default function SearchNavbarItem({
mobile,
}: Props): JSX.Element | null {
if (mobile) {
return null
}
return (
<NavbarSearch>
<SearchModalOpener />
</NavbarSearch>
)
}

View File

@@ -1,524 +0,0 @@
import React, { useEffect, useReducer, useRef, useState } from "react"
import clsx from "clsx"
import algoliaSearchHelper from "algoliasearch-helper"
import algoliaSearch from "algoliasearch/lite"
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment"
import Head from "@docusaurus/Head"
import Link from "@docusaurus/Link"
import { useAllDocsData } from "@docusaurus/plugin-content-docs/client"
import {
HtmlClassNameProvider,
useEvent,
usePluralForm,
useSearchQueryString,
} from "@docusaurus/theme-common"
import { useTitleFormatter } from "@docusaurus/theme-common/internal"
import Translate, { translate } from "@docusaurus/Translate"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import { useSearchResultUrlProcessor } from "@docusaurus/theme-search-algolia/client"
import Layout from "@theme/Layout"
import { ThemeConfig } from "@medusajs/docs"
import UserProvider from "@site/src/providers/User"
// Very simple pluralization: probably good enough for now
function useDocumentsFoundPlural() {
const { selectMessage } = usePluralForm()
return (count: number) =>
selectMessage(
count,
translate(
{
id: "theme.SearchPage.documentsFound.plurals",
description: `Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)`,
message: "One document found|{count} documents found",
},
{ count }
)
)
}
function useDocsSearchVersionsHelpers() {
const allDocsData = useAllDocsData()
// State of the version select menus / algolia facet filters
// docsPluginId -> versionName map
const [searchVersions, setSearchVersions] = useState<{
[pluginId: string]: string
}>(() =>
Object.entries(allDocsData).reduce(
(acc, [pluginId, pluginData]) => ({
...acc,
[pluginId]: pluginData.versions[0]!.name,
}),
{}
)
)
// Set the value of a single select menu
const setSearchVersion = (pluginId: string, searchVersion: string) =>
setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion }))
const versioningEnabled = Object.values(allDocsData).some(
(docsData) => docsData.versions.length > 1
)
return {
allDocsData,
versioningEnabled,
searchVersions,
setSearchVersion,
}
}
// We want to display one select per versioned docs plugin instance
function SearchVersionSelectList({
docsSearchVersionsHelpers,
}: {
docsSearchVersionsHelpers: ReturnType<typeof useDocsSearchVersionsHelpers>
}) {
const versionedPluginEntries = Object.entries(
docsSearchVersionsHelpers.allDocsData
)
// Do not show a version select for unversioned docs plugin instances
.filter(([, docsData]) => docsData.versions.length > 1)
return (
<div
className={clsx(
"col",
"col--3",
"pl-0",
"lg:!max-w-[unset] xs:!max-w-[40%] !max-w-full",
"xs:pl-0 pl-1"
)}
>
{versionedPluginEntries.map(([pluginId, docsData]) => {
const labelPrefix =
versionedPluginEntries.length > 1 ? `${pluginId}: ` : ""
return (
<select
key={pluginId}
onChange={(e) =>
docsSearchVersionsHelpers.setSearchVersion(
pluginId,
e.target.value
)
}
defaultValue={docsSearchVersionsHelpers.searchVersions[pluginId]}
className={clsx("search-page-input")}
>
{docsData.versions.map((version, i) => (
<option
key={i}
label={`${labelPrefix}${version.label}`}
value={version.name}
/>
))}
</select>
)
})}
</div>
)
}
type ResultDispatcherState = {
items: {
title: string
url: string
summary: string
breadcrumbs: string[]
}[]
query: string | null
totalResults: number | null
totalPages: number | null
lastPage: number | null
hasMore: boolean | null
loading: boolean | null
}
type ResultDispatcher =
| { type: "reset"; value?: undefined }
| { type: "loading"; value?: undefined }
| { type: "update"; value: ResultDispatcherState }
| { type: "advance"; value?: undefined }
function SearchPageContent(): JSX.Element {
const {
siteConfig: { themeConfig },
} = useDocusaurusContext()
const {
algolia: { appId, apiKey, indexName },
} = themeConfig as ThemeConfig
const processSearchResultUrl = useSearchResultUrlProcessor()
const documentsFoundPlural = useDocumentsFoundPlural()
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers()
const [searchQuery, setSearchQuery] = useSearchQueryString()
const initialSearchResultState: ResultDispatcherState = {
items: [],
query: null,
totalResults: null,
totalPages: null,
lastPage: null,
hasMore: null,
loading: null,
}
const [searchResultState, searchResultStateDispatcher] = useReducer(
(prevState: ResultDispatcherState, data: ResultDispatcher) => {
switch (data.type) {
case "reset": {
return initialSearchResultState
}
case "loading": {
return { ...prevState, loading: true }
}
case "update": {
if (searchQuery !== data.value.query) {
return prevState
}
return {
...data.value,
items:
data.value.lastPage === 0
? data.value.items
: prevState.items.concat(data.value.items),
}
}
case "advance": {
const hasMore = prevState.totalPages! > prevState.lastPage! + 1
return {
...prevState,
lastPage: hasMore ? prevState.lastPage! + 1 : prevState.lastPage,
hasMore,
}
}
default:
return prevState
}
},
initialSearchResultState
)
const algoliaClient = algoliaSearch(appId, apiKey)
const algoliaHelper = algoliaSearchHelper(algoliaClient, indexName, {
hitsPerPage: 15,
advancedSyntax: true,
disjunctiveFacets: ["language", "docusaurus_tag"],
})
algoliaHelper.on(
"result",
({ results: { query, hits, page, nbHits, nbPages } }) => {
if (query === "" || !Array.isArray(hits)) {
searchResultStateDispatcher({ type: "reset" })
return
}
const sanitizeValue = (value: string) =>
value.replace(
/algolia-docsearch-suggestion--highlight/g,
"search-result-match"
)
const items = hits.map(
({
url,
_highlightResult: { hierarchy },
_snippetResult: snippet = {},
}: {
url: string
_highlightResult: { hierarchy: { [key: string]: { value: string } } }
_snippetResult: { content?: { value: string } }
}) => {
const titles = Object.keys(hierarchy).map((key) =>
sanitizeValue(hierarchy[key]!.value)
)
return {
title: titles.pop()!,
url: processSearchResultUrl(url),
summary: snippet.content
? `${sanitizeValue(snippet.content.value)}...`
: "",
breadcrumbs: titles,
}
}
)
searchResultStateDispatcher({
type: "update",
value: {
items,
query,
totalResults: nbHits,
totalPages: nbPages,
lastPage: page,
hasMore: nbPages > page + 1,
loading: false,
},
})
}
)
const [loaderRef, setLoaderRef] = useState<HTMLDivElement | null>(null)
const prevY = useRef(0)
const observer = useRef(
ExecutionEnvironment.canUseIntersectionObserver &&
new IntersectionObserver(
(entries) => {
const {
isIntersecting,
boundingClientRect: { y: currentY },
} = entries[0]!
if (isIntersecting && prevY.current > currentY) {
searchResultStateDispatcher({ type: "advance" })
}
prevY.current = currentY
},
{ threshold: 1 }
)
)
const getTitle = () => {
return searchQuery
? translate(
{
id: "theme.SearchPage.existingResultsTitle",
message: `Search results for "{query}"`,
description: "The search page title for non-empty query",
},
{
query: searchQuery,
}
)
: translate({
id: "theme.SearchPage.emptyResultsTitle",
message: "Search the documentation",
description: "The search page title for empty query",
})
}
const makeSearch = useEvent((page?: number) => {
// These commented out line are from algolia's implementation
// we might need them in the future
// algoliaHelper.addDisjunctiveFacetRefinement("docusaurus_tag", "default")
// algoliaHelper.addDisjunctiveFacetRefinement("language", currentLocale)
// Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(
// ([pluginId, searchVersion]) => {
// algoliaHelper.addDisjunctiveFacetRefinement(
// "docusaurus_tag",
// `docs-${pluginId}-${searchVersion}`
// )
// }
// )
algoliaHelper
.setQuery(searchQuery)
.setPage(page || 0)
.search()
})
useEffect(() => {
if (!loaderRef) {
return undefined
}
const currentObserver = observer.current
if (currentObserver) {
currentObserver.observe(loaderRef)
return () => currentObserver.unobserve(loaderRef)
}
return () => true
}, [loaderRef])
useEffect(() => {
searchResultStateDispatcher({ type: "reset" })
if (searchQuery) {
searchResultStateDispatcher({ type: "loading" })
setTimeout(() => {
makeSearch()
}, 300)
}
}, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch])
useEffect(() => {
if (!searchResultState.lastPage || searchResultState.lastPage === 0) {
return
}
makeSearch(searchResultState.lastPage)
}, [makeSearch, searchResultState.lastPage])
return (
<Layout>
<Head>
<title>{useTitleFormatter(getTitle())}</title>
{/*
We should not index search pages
See https://github.com/facebook/docusaurus/pull/3233
*/}
<meta property="robots" content="noindex, follow" />
</Head>
<div className={clsx("container", "mt-2")}>
<h1>{getTitle()}</h1>
<form className="row" onSubmit={(e) => e.preventDefault()}>
<div
className={clsx(
"col",
"lg:max-w-[unset] xs:max-w-[60%] max-w-full",
{
"col--9": docsSearchVersionsHelpers.versioningEnabled,
"col--12": !docsSearchVersionsHelpers.versioningEnabled,
}
)}
>
<input
type="search"
name="q"
className={clsx(
"search-page-input",
"placeholder:text-medusa-fg-subtle dark:placeholder:text-medusa-fg-subtle-dark"
)}
placeholder={translate({
id: "theme.SearchPage.inputPlaceholder",
message: "Type your search here",
description: "The placeholder for search page input",
})}
aria-label={translate({
id: "theme.SearchPage.inputLabel",
message: "Search",
description: "The ARIA label for search page input",
})}
onChange={(e) => setSearchQuery(e.target.value)}
value={searchQuery}
autoComplete="off"
autoFocus
/>
</div>
{docsSearchVersionsHelpers.versioningEnabled && (
<SearchVersionSelectList
docsSearchVersionsHelpers={docsSearchVersionsHelpers}
/>
)}
</form>
<div className="row">
<div className={clsx("col", "col--8", "!text-compact-small-plus")}>
{!!searchResultState.totalResults &&
documentsFoundPlural(searchResultState.totalResults)}
</div>
</div>
{searchResultState.items.length > 0 ? (
<main>
{searchResultState.items.map(
({ title, url, summary, breadcrumbs }, i) => (
<article
key={i}
className={clsx(
"py-1 px-0 border-b border-t-0 border-x-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark",
"!max-w-[unset]"
)}
>
<h2 className={clsx("font-normal mb-0.5")}>
<Link
to={url}
dangerouslySetInnerHTML={{ __html: title }}
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark"
)}
/>
</h2>
{breadcrumbs.length > 0 && (
<nav aria-label="breadcrumbs">
<ul
className={clsx(
"mb-0 pl-0",
"!text-compact-x-small-plus text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark"
)}
>
{breadcrumbs.map((html, index) => (
<li
key={index}
className="breadcrumbs__item"
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: html }}
/>
))}
</ul>
</nav>
)}
{summary && (
<p
className={clsx("mt-0.5 mb-0 mx-0")}
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: summary }}
/>
)}
</article>
)
)}
</main>
) : (
[
searchQuery && !searchResultState.loading && (
<p key="no-results">
<Translate
id="theme.SearchPage.noResultsText"
description="The paragraph for empty search result"
>
No results were found
</Translate>
</p>
),
!!searchResultState.loading && (
<div
key="spinner"
className={clsx(
"w-3 h-3 border-[7px] border-solid border-[#eee] border-t-medusa-fg-base dark:border-t-medusa-fg-base-dark",
"rounded-[50%] animate-spin my-0 mx-auto"
)}
/>
),
]
)}
{searchResultState.hasMore && (
<div className={clsx("mt-2")} ref={setLoaderRef}>
<Translate
id="theme.SearchPage.fetchingNewResults"
description="The paragraph for fetching new search results"
>
Fetching new results...
</Translate>
</div>
)}
</div>
</Layout>
)
}
export default function SearchPage(): JSX.Element {
return (
<HtmlClassNameProvider className="">
<UserProvider>
<SearchPageContent />
</UserProvider>
</HtmlClassNameProvider>
)
}

View File

@@ -131,6 +131,12 @@ declare module "@medusajs/docs" {
export declare type NavbarAction = NavbarActionLink | NavbarActionButton
export declare type OptionType = {
value: string
label: string
isAllOption?: boolean
}
export declare type ThemeConfig = {
reportCodeLinkPrefix?: string
footerFeedback: {
@@ -154,6 +160,20 @@ declare module "@medusajs/docs" {
magicComments: MagicCommentConfig[]
}
mobileLogo: NavbarLogo
algoliaConfig?: {
apiKey: string
indexNames: {
docs: string
api: string
}
appId: string
filters: OptionType[]
defaultFilters: string[]
defaultFiltersByPath: {
path: string
filters: string[]
}[]
}
} & DocusaurusThemeConfig
export declare type MedusaDocusaurusConfig = {

View File

@@ -0,0 +1,10 @@
export default function checkArraySameElms(
arr1: Array<any>,
arr2: Array<any>
): boolean {
if (arr1.length !== arr2.length) {
return false
}
return arr1.every((value, index) => value === arr2[index])
}

View File

@@ -0,0 +1,29 @@
export function findPrevSibling(
element: HTMLElement,
selector: string
): HTMLElement | null {
let prevElement = element.previousElementSibling
while (prevElement !== null) {
if (prevElement.matches(selector)) {
return prevElement as HTMLElement
}
prevElement = prevElement.previousElementSibling
}
return null
}
export function findNextSibling(
element: HTMLElement,
selector: string
): HTMLElement | null {
let nextElement = element.nextElementSibling
while (nextElement !== null) {
if (nextElement.matches(selector)) {
return nextElement as HTMLElement
}
nextElement = nextElement.nextElementSibling
}
return null
}