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

* initial implementation of search modal

* added hit and search suggestions

* added support for multiple indices

* updated sample env

* added close when click outside dropdown

* test for mobile

* added mobile design

* added shortcut

* dark mode fixes

* added search to docs

* added plugins filter

* added React import

* moved filters to configurations

* handled error on page load

* change suggestion text

* removed hits limit

* handle select all

* open link in current tab

* change highlight colors

* added support for shortcuts + auto focus

* change header and footer

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

View File

@@ -13,6 +13,7 @@ export type BadgeProps = {
| "blue"
| "blue-dark"
| "red"
| "neutral"
} & React.HTMLAttributes<HTMLSpanElement>
const Badge: React.FC<BadgeProps> = ({ className, variant, children }) => {
@@ -38,6 +39,8 @@ const Badge: React.FC<BadgeProps> = ({ className, variant, children }) => {
"bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text-dark border-medusa-tag-blue-border-dark",
variant === "red" &&
"bg-medusa-tag-red-bg dark:bg-medusa-tag-red-bg-dark text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark border-medusa-tag-red-border dark:border-medusa-tag-red-border-dark",
variant === "neutral" &&
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"badge",
className
)}

View File

@@ -0,0 +1,23 @@
import React from "react"
import clsx from "clsx"
type BorderedProps = {
wrapperClassName?: string
} & React.HTMLAttributes<HTMLSpanElement>
const Bordered: React.FC<BorderedProps> = ({ wrapperClassName, children }) => {
return (
<span
className={clsx(
"border-medusa-border-strong dark:border-medusa-border-strong-dark bg-docs-bg",
"dark:bg-docs-bg-dark mr-1 inline-flex w-fit items-center justify-center rounded border border-solid p-[3px]",
"no-zoom-img",
wrapperClassName
)}
>
{children}
</span>
)
}
export default Bordered

View File

@@ -0,0 +1,56 @@
import React from "react"
import clsx from "clsx"
import Bordered from "../Bordered/index"
import IconProps from "../Icons/types"
import { useColorMode } from "../../providers/color-mode"
import Image from "next/image"
type BorderedIconProp = {
icon?: {
light: string
dark?: string
}
IconComponent?: React.FC<IconProps>
wrapperClassName?: string
iconWrapperClassName?: string
iconClassName?: string
iconColorClassName?: string
} & React.HTMLAttributes<HTMLSpanElement>
const BorderedIcon: React.FC<BorderedIconProp> = ({
icon = null,
IconComponent = null,
wrapperClassName,
iconWrapperClassName,
iconClassName,
iconColorClassName = "",
}) => {
const { colorMode } = useColorMode()
return (
<Bordered wrapperClassName={wrapperClassName}>
<span
className={clsx(
"rounded-xs p-0.125 bg-medusa-bg-component dark:bg-medusa-bg-component-dark inline-flex items-center justify-center",
iconWrapperClassName
)}
>
{!IconComponent && (
<Image
src={(colorMode === "light" ? icon?.light : icon?.dark) || ""}
className={clsx(iconClassName, "bordered-icon")}
alt=""
/>
)}
{IconComponent && (
<IconComponent
className={clsx(iconClassName, "bordered-icon")}
iconColorClassName={iconColorClassName}
/>
)}
</span>
</Bordered>
)
}
export default BorderedIcon

View File

@@ -3,8 +3,8 @@ import clsx from "clsx"
export type ButtonProps = {
isSelected?: boolean
disabled?: boolean
variant?: "primary" | "secondary"
darkVariant?: "primary" | "secondary"
variant?: "primary" | "secondary" | "clear"
darkVariant?: "primary" | "secondary" | "clear"
} & React.HTMLAttributes<HTMLButtonElement>
const Button = ({
@@ -19,8 +19,10 @@ const Button = ({
className={clsx(
variant === "primary" && "btn-primary",
variant === "secondary" && "btn-secondary",
variant === "clear" && "btn-clear",
darkVariant && darkVariant === "primary" && "dark:btn-primary",
darkVariant && darkVariant === "secondary" && "dark:btn-secondary",
darkVariant && darkVariant === "clear" && "dark:btn-clear",
className
)}
{...props}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
import IconProps from "../types"
const IconDocumentTextSolid = ({ iconColorClassName, ...props }: IconProps) => {
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
fillRule="evenodd"
clipRule="evenodd"
d="M4.91521 1.625C4.08888 1.625 3.41968 2.295 3.41968 3.12054V16.8795C3.41968 17.705 4.08968 18.375 4.91521 18.375H15.0849C15.9104 18.375 16.5804 17.705 16.5804 16.8795V10.5982C16.5804 9.80493 16.2653 9.04414 15.7043 8.48321C15.1434 7.92227 14.3826 7.60714 13.5893 7.60714H12.0938C11.6971 7.60714 11.3167 7.44958 11.0363 7.16911C10.7558 6.88864 10.5982 6.50825 10.5982 6.11161V4.61607C10.5982 3.82279 10.2831 3.062 9.72218 2.50106C9.16125 1.94013 8.40046 1.625 7.60718 1.625H4.91521ZM6.41075 12.3929C6.41075 12.2342 6.47378 12.082 6.58596 11.9699C6.69815 11.8577 6.85031 11.7946 7.00896 11.7946H12.9911C13.1498 11.7946 13.3019 11.8577 13.4141 11.9699C13.5263 12.082 13.5893 12.2342 13.5893 12.3929C13.5893 12.5515 13.5263 12.7037 13.4141 12.8159C13.3019 12.928 13.1498 12.9911 12.9911 12.9911H7.00896C6.85031 12.9911 6.69815 12.928 6.58596 12.8159C6.47378 12.7037 6.41075 12.5515 6.41075 12.3929ZM7.00896 14.1875C6.85031 14.1875 6.69815 14.2505 6.58596 14.3627C6.47378 14.4749 6.41075 14.6271 6.41075 14.7857C6.41075 14.9444 6.47378 15.0965 6.58596 15.2087C6.69815 15.3209 6.85031 15.3839 7.00896 15.3839H10C10.1587 15.3839 10.3108 15.3209 10.423 15.2087C10.5352 15.0965 10.5982 14.9444 10.5982 14.7857C10.5982 14.6271 10.5352 14.4749 10.423 14.3627C10.3108 14.2505 10.1587 14.1875 10 14.1875H7.00896Z"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
<path
d="M10.7745 1.8772C11.4338 2.6373 11.7961 3.61006 11.7947 4.61622V6.11176C11.7947 6.27686 11.9287 6.41086 12.0938 6.41086H13.5893C14.5955 6.40947 15.5683 6.77177 16.3284 7.43102C15.9774 6.09637 15.2783 4.87888 14.3025 3.90306C13.3267 2.92724 12.1092 2.22812 10.7745 1.8772Z"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
</svg>
)
}
export default IconDocumentTextSolid

View File

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

View File

@@ -0,0 +1,29 @@
import IconProps from "../types"
const IconExclamationCircleSolid = ({
iconColorClassName,
...props
}: IconProps) => {
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
fillRule="evenodd"
clipRule="evenodd"
d="M18 10C18 12.1217 17.1571 14.1566 15.6569 15.6569C14.1566 17.1571 12.1217 18 10 18C7.87827 18 5.84344 17.1571 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C12.1217 2 14.1566 2.84285 15.6569 4.34315C17.1571 5.84344 18 7.87827 18 10ZM10 5C10.1989 5 10.3897 5.07902 10.5303 5.21967C10.671 5.36032 10.75 5.55109 10.75 5.75V10.25C10.75 10.4489 10.671 10.6397 10.5303 10.7803C10.3897 10.921 10.1989 11 10 11C9.80109 11 9.61032 10.921 9.46967 10.7803C9.32902 10.6397 9.25 10.4489 9.25 10.25V5.75C9.25 5.55109 9.32902 5.36032 9.46967 5.21967C9.61032 5.07902 9.80109 5 10 5ZM10 15C10.2652 15 10.5196 14.8946 10.7071 14.7071C10.8946 14.5196 11 14.2652 11 14C11 13.7348 10.8946 13.4804 10.7071 13.2929C10.5196 13.1054 10.2652 13 10 13C9.73478 13 9.48043 13.1054 9.29289 13.2929C9.10536 13.4804 9 13.7348 9 14C9 14.2652 9.10536 14.5196 9.29289 14.7071C9.48043 14.8946 9.73478 15 10 15Z"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
</svg>
)
}
export default IconExclamationCircleSolid

View File

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

View File

@@ -0,0 +1,42 @@
import IconProps from "../types"
const IconToolsSolid = ({ iconColorClassName, ...props }: IconProps) => {
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
fillRule="evenodd"
clipRule="evenodd"
d="M9.99753 6.06351C9.99737 5.44855 10.1413 4.84211 10.4177 4.29277C10.6941 3.74343 11.0953 3.26648 11.5893 2.90013C12.0832 2.53379 12.6561 2.28824 13.262 2.18315C13.8679 2.07807 14.49 2.11638 15.0784 2.29501C15.1714 2.32318 15.2556 2.37498 15.3226 2.44534C15.3896 2.5157 15.4373 2.60222 15.4609 2.69649C15.4846 2.79075 15.4834 2.88952 15.4575 2.98319C15.4316 3.07686 15.3819 3.16222 15.3132 3.23095L12.8233 5.72003C12.8706 6.07626 13.0303 6.42049 13.3041 6.69422C13.5778 6.96795 13.922 7.12769 14.279 7.17419L16.7673 4.6851C16.8361 4.6164 16.9214 4.56668 17.0151 4.54078C17.1088 4.51489 17.2075 4.5137 17.3018 4.53734C17.3961 4.56099 17.4826 4.60864 17.5529 4.67568C17.6233 4.74271 17.6751 4.82683 17.7033 4.91984C17.8902 5.53544 17.9234 6.18746 17.8 6.81886C17.6766 7.45026 17.4003 8.0418 16.9954 8.54174C16.5905 9.04167 16.0692 9.43477 15.4772 9.68663C14.8852 9.93849 14.2405 10.0415 13.5995 9.9865C12.8361 9.92201 12.1971 10.0615 11.8679 10.462L6.50576 16.9753C6.2862 17.2406 6.01366 17.4571 5.70559 17.611C5.39752 17.7649 5.06071 17.8528 4.71672 17.869C4.37273 17.8852 4.02915 17.8294 3.70797 17.7052C3.38679 17.5809 3.0951 17.391 2.85157 17.1475C2.60803 16.904 2.41803 16.6124 2.29372 16.2912C2.16941 15.9701 2.11353 15.6265 2.12967 15.2825C2.14581 14.9385 2.2336 14.6017 2.38743 14.2936C2.54126 13.9855 2.75773 13.7129 3.02299 13.4933L9.53556 8.13037C9.93528 7.8004 10.0755 7.16219 10.011 6.39874C10.0018 6.28723 9.99734 6.17539 9.99753 6.06351ZM4.08567 15.3442C4.08567 15.195 4.14493 15.0519 4.25041 14.9464C4.35589 14.8409 4.49895 14.7817 4.64813 14.7817H4.65413C4.8033 14.7817 4.94637 14.8409 5.05185 14.9464C5.15733 15.0519 5.21659 15.195 5.21659 15.3442V15.3501C5.21659 15.4993 5.15733 15.6424 5.05185 15.7479C4.94637 15.8534 4.8033 15.9126 4.65413 15.9126H4.64813C4.49895 15.9126 4.35589 15.8534 4.25041 15.7479C4.14493 15.6424 4.08567 15.4993 4.08567 15.3501V15.3442Z"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
<path
d="M8.55462 7.48082L6.90398 5.83093V4.65651C6.90397 4.5594 6.87883 4.46395 6.83099 4.37945C6.78315 4.29495 6.71425 4.22427 6.631 4.17429L3.81868 2.4869C3.71112 2.42242 3.58509 2.39574 3.46063 2.41109C3.33616 2.42645 3.2204 2.48296 3.13173 2.57164L2.56926 3.13411C2.48058 3.22278 2.42407 3.33854 2.40871 3.46301C2.39336 3.58747 2.42004 3.7135 2.48452 3.82106L4.17191 6.63338C4.22189 6.71663 4.29257 6.78553 4.37707 6.83337C4.46157 6.88121 4.55702 6.90635 4.65413 6.90636H5.82705L7.37345 8.45276L8.55462 7.48007V7.48082Z"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.4146 13.9971L13.5516 17.1334C13.7866 17.3685 14.0657 17.5549 14.3727 17.6821C14.6798 17.8093 15.009 17.8748 15.3414 17.8748C15.6737 17.8748 16.0029 17.8093 16.31 17.6821C16.617 17.5549 16.8961 17.3685 17.1311 17.1334C17.3661 16.8984 17.5526 16.6194 17.6798 16.3123C17.807 16.0052 17.8724 15.6761 17.8724 15.3437C17.8724 15.0113 17.807 14.6821 17.6798 14.3751C17.5526 14.068 17.3661 13.7889 17.1311 13.5539L14.6518 11.0753C14.2717 11.1292 13.8868 11.14 13.5043 11.1076C13.2089 11.0821 12.9929 11.1031 12.8541 11.1391C12.8102 11.1484 12.768 11.1643 12.7289 11.1863L10.4146 13.9971ZM12.9749 12.9772C13.0803 12.8719 13.2233 12.8127 13.3724 12.8127C13.5214 12.8127 13.6644 12.8719 13.7698 12.9772L15.176 14.3841C15.2312 14.4356 15.2756 14.4977 15.3063 14.5667C15.3371 14.6357 15.3536 14.7102 15.3549 14.7857C15.3563 14.8612 15.3424 14.9362 15.3141 15.0063C15.2858 15.0763 15.2437 15.1399 15.1903 15.1933C15.1369 15.2467 15.0732 15.2888 15.0032 15.3171C14.9332 15.3454 14.8581 15.3593 14.7826 15.358C14.7071 15.3566 14.6326 15.3401 14.5636 15.3094C14.4946 15.2786 14.4325 15.2343 14.381 15.179L12.9749 13.7729C12.8696 13.6674 12.8104 13.5245 12.8104 13.3754C12.8104 13.2264 12.8696 13.0834 12.9749 12.9779V12.9772Z"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
</svg>
)
}
export default IconToolsSolid

View File

@@ -0,0 +1,36 @@
import IconProps from "../types"
const IconTreeNode = ({ iconColorClassName, ...props }: IconProps) => {
return (
<svg
width={props.width || 16}
height={props.height || 32}
viewBox="0 0 16 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect
width="1"
height="32"
rx="0.5"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
<rect
y="15.5"
width="16"
height="1"
rx="0.5"
className={
iconColorClassName ||
"fill-medusa-fg-subtle dark:fill-medusa-fg-subtle-dark"
}
/>
</svg>
)
}
export default IconTreeNode

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
import clsx from "clsx"
type KbdProps = React.ComponentProps<"kbd">
const Kbd = ({ children, className, ...props }: KbdProps) => {
return (
<kbd
className={clsx(
"h-[22px] w-[22px] rounded-sm p-0",
"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",
className
)}
{...props}
>
{children}
</kbd>
)
}
export default Kbd

View File

@@ -4,6 +4,7 @@ import type { OpenAPIV3 } from "openapi-types"
import Link from "./Link"
import CodeWrapper from "./CodeWrapper"
import H2 from "./H2"
import Kbd from "./Kbd"
export type ScopeType = {
specs?: OpenAPIV3.Document
@@ -16,6 +17,7 @@ const getCustomComponents = (scope?: ScopeType): MDXComponents => {
code: CodeWrapper,
a: Link,
h2: (props) => <H2 addToSidebar={scope?.addToSidebar} {...props} />,
kbd: Kbd,
}
}

View File

@@ -2,11 +2,12 @@ import clsx from "clsx"
import Button, { ButtonProps } from "../../Button"
type ModalFooterProps = {
actions: ButtonProps[]
actions?: ButtonProps[]
children?: React.ReactNode
className?: string
}
const ModalFooter = ({ actions, className }: ModalFooterProps) => {
const ModalFooter = ({ actions, children, className }: ModalFooterProps) => {
return (
<div
className={clsx(
@@ -16,9 +17,10 @@ const ModalFooter = ({ actions, className }: ModalFooterProps) => {
className
)}
>
{actions.map((action, index) => (
{actions?.map((action, index) => (
<Button {...action} key={index} />
))}
{children}
</div>
)
}

View File

@@ -1,6 +1,7 @@
import clsx from "clsx"
import { useModal } from "../../../providers/modal"
import IconXMark from "../../Icons/XMark"
import Button from "../../Button"
type ModalHeaderProps = {
title?: string
@@ -23,9 +24,13 @@ const ModalHeader = ({ title }: ModalHeaderProps) => {
>
{title}
</span>
<button className="btn-clear cursor-pointer" onClick={() => closeModal()}>
<Button
variant="clear"
className="cursor-pointer"
onClick={() => closeModal()}
>
<IconXMark />
</button>
</Button>
</div>
)
}

View File

@@ -1,70 +1,117 @@
import clsx from "clsx"
import React, { useRef } from "react"
import React, { forwardRef, useCallback, useEffect, useRef } from "react"
import { ButtonProps } from "../Button"
import { useModal } from "../../providers/modal"
import ModalHeader from "./Header"
import ModalFooter from "./Footer"
import useKeyboardShortcut from "../../hooks/use-keyboard-shortcut"
export type ModalProps = {
className?: string
title?: string
actions?: ButtonProps[]
modalContainerClassName?: string
contentClassName?: string
} & React.DetailedHTMLProps<
React.DialogHTMLAttributes<HTMLDialogElement>,
HTMLDialogElement
>
onClose?: React.ReactEventHandler<HTMLDialogElement>
open?: boolean
footerContent?: React.ReactNode
} & Omit<React.ComponentProps<"dialog">, "ref">
const Modal: React.FC<ModalProps> = ({
className,
title,
actions,
children,
contentClassName,
...props
}) => {
const Modal = forwardRef<HTMLDialogElement, ModalProps>(function Modal(
{
className,
title,
actions,
children,
contentClassName,
modalContainerClassName,
onClose,
open = true,
footerContent,
...props
},
passedRef
) {
const { closeModal } = useModal()
const dialogRef = useRef<HTMLDialogElement>(null)
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 === dialogRef.current) {
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])
return (
<dialog
{...props}
className={clsx(
"fixed top-0 left-0 flex h-screen w-screen items-center justify-center",
"z-[500] bg-transparent",
"bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark z-[500]",
"hidden open:flex",
className
)}
onClick={handleClick}
ref={dialogRef}
ref={setRefs}
onClose={handleClose}
open={open}
>
<div
className={clsx(
"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-[560px]"
"w-[90%] md:h-auto md:w-[75%] lg:w-[560px]",
modalContainerClassName
)}
>
<ModalHeader title={title} />
<div
className={clsx(
"overflow-auto py-1.5 px-2 lg:min-h-[400px]",
contentClassName
)}
>
{title && <ModalHeader title={title} />}
<div className={clsx("overflow-auto py-1.5 px-2", contentClassName)}>
{children}
</div>
{actions && actions?.length > 0 && <ModalFooter actions={actions} />}
{footerContent && <ModalFooter>{footerContent}</ModalFooter>}
</div>
</dialog>
)
}
})
export default Modal

View File

@@ -1,13 +1,18 @@
"use client"
import { useModal } from "../../../providers/modal"
import { usePageLoading } from "../../../providers/page-loading"
import Button from "../../Button"
import DetailedFeedback from "../../DetailedFeedback"
const FeedbackModal = () => {
const { setModalProps } = useModal()
const { isLoading } = usePageLoading()
const openModal = () => {
if (isLoading) {
return
}
setModalProps({
title: "Send your Feedback",
children: <DetailedFeedback />,

View File

@@ -5,20 +5,22 @@ import { useSidebar } from "@/providers/sidebar"
import IconSidebar from "../../Icons/Sidebar"
import clsx from "clsx"
import IconXMark from "../../Icons/XMark"
import { usePageLoading } from "../../../providers/page-loading"
type NavbarMenuButtonProps = {
buttonProps?: NavbarIconButtonProps
}
const NavbarMenuButton = ({ buttonProps }: NavbarMenuButtonProps) => {
const { items, setMobileSidebarOpen, mobileSidebarOpen } = useSidebar()
const { setMobileSidebarOpen, mobileSidebarOpen } = useSidebar()
const { isLoading } = usePageLoading()
return (
<NavbarIconButton
{...buttonProps}
className={clsx("mr-1 lg:!hidden", buttonProps?.className)}
onClick={() => {
if (items.top.length !== 0 && items.bottom.length !== 0) {
if (!isLoading) {
setMobileSidebarOpen((prevValue) => !prevValue)
}
}}

View File

@@ -1,33 +1,13 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import NavbarMenuButton from "../MenuButton"
import NavbarMobileLogo from "../MobileLogo"
import SearchBar from "../../SearchBar"
import NavbarColorModeToggle from "../ColorModeToggle"
import SearchModalOpener from "../../Search/ModalOpener"
import { useMobile } from "../../../providers/mobile"
const MobileMenu = () => {
const [isMobile, setIsMobile] = useState(false)
const handleResize = useCallback(() => {
if (window.innerWidth < 1025 && !isMobile) {
setIsMobile(true)
} else if (window.innerWidth >= 1025 && isMobile) {
setIsMobile(false)
}
}, [isMobile])
useEffect(() => {
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
}
}, [handleResize])
useEffect(() => {
handleResize()
}, [])
const { isMobile } = useMobile()
return (
<div className="flex w-full items-center justify-between lg:hidden">
@@ -41,7 +21,7 @@ const MobileMenu = () => {
/>
<NavbarMobileLogo />
<div className="flex">
<SearchBar />
<SearchModalOpener />
<NavbarColorModeToggle
buttonProps={{
className:

View File

@@ -2,12 +2,10 @@ import clsx from "clsx"
import NavbarLink from "./Link"
import NavbarColorModeToggle from "./ColorModeToggle"
import NavbarLogo from "./Logo"
import SearchBar from "../SearchBar"
import NavbarMenuButton from "./MenuButton"
import getLinkWithBasePath from "../../utils/get-link-with-base-path"
import FeedbackModal from "./FeedbackModal"
import NavbarMobileLogo from "./MobileLogo"
import MobileMenu from "./MobileMenu"
import SearchModalOpener from "../Search/ModalOpener"
const Navbar = () => {
return (
@@ -44,8 +42,8 @@ const Navbar = () => {
</div>
</div>
<div className="hidden min-w-0 flex-1 items-center justify-end gap-0.5 lg:flex">
<div className="w-[240px] [&>*]:flex-1">
<SearchBar />
<div>
<SearchModalOpener />
</div>
<NavbarColorModeToggle />
<FeedbackModal />

View File

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

View File

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

View File

@@ -0,0 +1,209 @@
import clsx from "clsx"
import Link from "next/link"
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 getBaseUrl from "../../../utils/get-base-url"
import { useSearch } from "../../../providers/search"
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 indices = useMemo(
() => [
process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp",
process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp",
],
[]
)
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()
const { setIsOpen } = useSearch()
// 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) || ""
)
}
const baseUrl = useMemo(() => getBaseUrl(), [])
const checkIfInternal = (url: string): boolean => {
const testRegex = new RegExp(`^${baseUrl}/api/(admin|store)`)
return testRegex.test(url)
}
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"
onClick={(e) => {
if (checkIfInternal(item.url)) {
e.preventDefault()
window.location.href = item.url
setIsOpen(false)
}
}}
/>
</div>
))}
</Fragment>
))}
</div>
)
}
export default SearchHitsWrapper

View File

@@ -0,0 +1,333 @@
"use client"
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 IconMagnifyingGlass from "../../Icons/MagnifyingGlass"
import IconXMark from "../../Icons/XMark"
import SearchEmptyQueryBoundary from "../EmptyQueryBoundary"
import SearchSuggestions from "../Suggestions"
import { useSearch } from "../../../providers/search"
import checkArraySameElms from "../../../utils/array-same-elms"
import SearchHitsWrapper from "../Hits"
import Button from "../../Button"
import Kbd from "../../MDXComponents/Kbd"
import { OptionType } from "../../../hooks/use-select"
import SelectBadge from "../../Select/Badge"
import useKeyboardShortcut from "../../../hooks/use-keyboard-shortcut"
import { findNextSibling, findPrevSibling } from "../../../utils/dom-utils"
const algoliaClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "temp",
process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp"
)
const searchClient: SearchClient = {
...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 SearchModal = () => {
const modalRef = useRef<HTMLDialogElement | null>(null)
const options: OptionType[] = useMemo(() => {
return [
{
value: "admin",
label: "Admin API",
},
{
value: "store",
label: "Store API",
},
{
value: "docs",
label: "Docs",
},
{
value: "user-guide",
label: "User Guide",
},
{
value: "plugins",
label: "Plugins",
},
{
value: "reference",
label: "References",
},
{
value: "ui",
label: "UI",
},
]
}, [])
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={process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME}
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-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 active:outline-none focus:outline-none"
),
submit: clsx("absolute top-[18px] left-1"),
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"
),
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-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-medusa-border-base dark:border-medusa-border-base-dark border-t",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark"
)}
>
<SelectBadge
multiple
options={options}
value={filters}
setSelected={(value) =>
setFilters(Array.isArray(value) ? [...value] : [value])
}
addSelected={(value) => setFilters((prev) => [...prev, value])}
removeSelected={(value) =>
setFilters((prev) => prev.filter((v) => v !== value))
}
showClearButton={false}
placeholder="Filters"
handleAddAll={(isAllSelected: boolean) => {
if (isAllSelected) {
setFilters(defaultFilters)
} else {
setFilters(options.map((option) => option.value))
}
}}
/>
<div className="hidden items-center gap-1 md:flex">
<div className="flex items-center gap-0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Navigation
</span>
<span className="gap-0.25 flex">
<Kbd></Kbd>
<Kbd></Kbd>
</span>
</div>
<div className="flex items-center gap-0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Open Result
</span>
<Kbd></Kbd>
</div>
</div>
</div>
</Modal>
)
}
export default SearchModal

View File

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

View File

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

View File

@@ -0,0 +1,49 @@
import clsx from "clsx"
import { useInstantSearch } from "react-instantsearch"
import SearchHitGroupName from "../Hits/GroupName"
const SearchSuggestions = () => {
const { setIndexUiState } = useInstantSearch()
const suggestions = [
"Authentication",
"Expanding fields",
"Selecting fields",
"Pagination",
"Query parameter types",
]
return (
<div className="h-full overflow-auto">
<SearchHitGroupName name={"Search suggestions"} />
{suggestions.map((suggestion, index) => (
<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: suggestion,
})
}
key={index}
tabIndex={index}
data-hit
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-small"
)}
>
{suggestion}
</span>
</div>
))}
</div>
)
}
export default SearchSuggestions

View File

@@ -1,20 +0,0 @@
"use client"
import { DocSearch } from "@docsearch/react"
import "@docsearch/css"
const SearchBar = () => {
return (
<DocSearch
appId={process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "temp"}
indexName={process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || "temp"}
apiKey={process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "temp"}
searchParameters={{
tagFilters: ["api"],
}}
/>
)
}
export default SearchBar

View File

@@ -0,0 +1,135 @@
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 w-[60px] 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",
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-subtle-hover-dark",
"py-0.25 h-fit cursor-pointer px-0.5",
"flex items-center gap-[6px] whitespace-nowrap",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
!open && "bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark",
open &&
"bg-medusa-bg-subtle-hover dark:bg-medusa-bg-subtle-hover-dark",
className
)}
ref={ref}
onClick={(e) => {
if (!dropdownRef.current?.contains(e.target as Element)) {
setOpen((prev) => !prev)
}
}}
>
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Show results from:{" "}
</span>
{getSelectedText()}
</div>
<input
type="hidden"
name={props.name}
value={Array.isArray(value) ? value.join(",") : value}
/>
<SelectDropdown
options={options}
open={open}
setOpen={setOpen}
addAll={addAll}
multiple={multiple}
isAllSelected={isAllSelected}
isValueSelected={isValueSelected}
handleSelectAll={handleSelectAll}
handleChange={handleChange}
parentRef={ref}
ref={dropdownRef}
className={clsx(
"!top-[unset] !bottom-full",
open && "!-translate-y-0.5"
)}
/>
</div>
)
}
export default SelectBadge

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ const TextArea = (props: TextAreaProps) => {
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"border-medusa-border-loud-muted dark:border-medusa-border-loud-muted-dark rounded-sm border border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-sm border border-solid",
"pt-0.4 px-0.75 text-medium font-base 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",