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:
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
29
www/docs/src/components/Modal/Footer/index.tsx
Normal file
29
www/docs/src/components/Modal/Footer/index.tsx
Normal 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
|
||||
39
www/docs/src/components/Modal/Header/index.tsx
Normal file
39
www/docs/src/components/Modal/Header/index.tsx
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
22
www/docs/src/components/Search/EmptyQueryBoundary/index.tsx
Normal file
22
www/docs/src/components/Search/EmptyQueryBoundary/index.tsx
Normal 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
|
||||
22
www/docs/src/components/Search/Hits/GroupName/index.tsx
Normal file
22
www/docs/src/components/Search/Hits/GroupName/index.tsx
Normal 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
|
||||
190
www/docs/src/components/Search/Hits/index.tsx
Normal file
190
www/docs/src/components/Search/Hits/index.tsx
Normal 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
|
||||
312
www/docs/src/components/Search/Modal/index.tsx
Normal file
312
www/docs/src/components/Search/Modal/index.tsx
Normal 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
|
||||
78
www/docs/src/components/Search/ModalOpener/index.tsx
Normal file
78
www/docs/src/components/Search/ModalOpener/index.tsx
Normal 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
|
||||
15
www/docs/src/components/Search/NoResults/index.tsx
Normal file
15
www/docs/src/components/Search/NoResults/index.tsx
Normal 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
|
||||
69
www/docs/src/components/Search/Suggestions/index.tsx
Normal file
69
www/docs/src/components/Search/Suggestions/index.tsx
Normal 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
|
||||
136
www/docs/src/components/Select/Badge/index.tsx
Normal file
136
www/docs/src/components/Select/Badge/index.tsx
Normal 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
|
||||
152
www/docs/src/components/Select/Dropdown/index.tsx
Normal file
152
www/docs/src/components/Select/Dropdown/index.tsx
Normal 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
|
||||
128
www/docs/src/components/Select/Input/index.tsx
Normal file
128
www/docs/src/components/Select/Input/index.tsx
Normal 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
|
||||
9
www/docs/src/components/Select/types.ts
Normal file
9
www/docs/src/components/Select/types.ts
Normal 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">
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
64
www/docs/src/hooks/use-keyboard-shortcut.tsx
Normal file
64
www/docs/src/hooks/use-keyboard-shortcut.tsx
Normal 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
|
||||
93
www/docs/src/hooks/use-select.tsx
Normal file
93
www/docs/src/hooks/use-select.tsx
Normal 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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
68
www/docs/src/providers/Search/index.tsx
Normal file
68
www/docs/src/providers/Search/index.tsx
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
41
www/docs/src/theme/Icon/ArrowDownLeftMini/index.tsx
Normal file
41
www/docs/src/theme/Icon/ArrowDownLeftMini/index.tsx
Normal 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
|
||||
31
www/docs/src/theme/Icon/CheckMini/index.tsx
Normal file
31
www/docs/src/theme/Icon/CheckMini/index.tsx
Normal 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
|
||||
31
www/docs/src/theme/Icon/ChevronUpDown/index.tsx
Normal file
31
www/docs/src/theme/Icon/ChevronUpDown/index.tsx
Normal 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
|
||||
30
www/docs/src/theme/Icon/EllipseMiniSolid/index.tsx
Normal file
30
www/docs/src/theme/Icon/EllipseMiniSolid/index.tsx
Normal 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
|
||||
31
www/docs/src/theme/Icon/MagnifyingGlass/index.tsx
Normal file
31
www/docs/src/theme/Icon/MagnifyingGlass/index.tsx
Normal 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
|
||||
31
www/docs/src/theme/Icon/XMarkMini/index.tsx
Normal file
31
www/docs/src/theme/Icon/XMarkMini/index.tsx
Normal 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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
27
www/docs/src/theme/MDXComponents/Kbd.tsx
Normal file
27
www/docs/src/theme/MDXComponents/Kbd.tsx
Normal 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
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
10
www/docs/src/theme/Navbar/Search/index.tsx
Normal file
10
www/docs/src/theme/Navbar/Search/index.tsx
Normal 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>
|
||||
}
|
||||
18
www/docs/src/theme/NavbarItem/SearchNavbarItem.tsx
Normal file
18
www/docs/src/theme/NavbarItem/SearchNavbarItem.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
20
www/docs/src/types/index.d.ts
vendored
20
www/docs/src/types/index.d.ts
vendored
@@ -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 = {
|
||||
|
||||
10
www/docs/src/utils/array-same-elms.ts
Normal file
10
www/docs/src/utils/array-same-elms.ts
Normal 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])
|
||||
}
|
||||
29
www/docs/src/utils/dom-utils.ts
Normal file
29
www/docs/src/utils/dom-utils.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user