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:
@@ -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
|
||||
)}
|
||||
|
||||
23
www/api-reference/components/Bordered/index.tsx
Normal file
23
www/api-reference/components/Bordered/index.tsx
Normal 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
|
||||
56
www/api-reference/components/BorderedIcon/index.tsx
Normal file
56
www/api-reference/components/BorderedIcon/index.tsx
Normal 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
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
32
www/api-reference/components/Icons/CheckMini/index.tsx
Normal file
32
www/api-reference/components/Icons/CheckMini/index.tsx
Normal 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
|
||||
32
www/api-reference/components/Icons/ChevronUpDown/index.tsx
Normal file
32
www/api-reference/components/Icons/ChevronUpDown/index.tsx
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
27
www/api-reference/components/Icons/MagnifyingGlass/index.tsx
Normal file
27
www/api-reference/components/Icons/MagnifyingGlass/index.tsx
Normal 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
|
||||
42
www/api-reference/components/Icons/ToolsSolid/index.tsx
Normal file
42
www/api-reference/components/Icons/ToolsSolid/index.tsx
Normal 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
|
||||
36
www/api-reference/components/Icons/TreeNode/index.tsx
Normal file
36
www/api-reference/components/Icons/TreeNode/index.tsx
Normal 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
|
||||
27
www/api-reference/components/Icons/XMarkMini/index.tsx
Normal file
27
www/api-reference/components/Icons/XMarkMini/index.tsx
Normal 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
|
||||
@@ -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",
|
||||
|
||||
24
www/api-reference/components/MDXComponents/Kbd/index.tsx
Normal file
24
www/api-reference/components/MDXComponents/Kbd/index.tsx
Normal 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
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
21
www/api-reference/components/Search/Hits/GroupName/index.tsx
Normal file
21
www/api-reference/components/Search/Hits/GroupName/index.tsx
Normal 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
|
||||
209
www/api-reference/components/Search/Hits/index.tsx
Normal file
209
www/api-reference/components/Search/Hits/index.tsx
Normal 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
|
||||
333
www/api-reference/components/Search/Modal/index.tsx
Normal file
333
www/api-reference/components/Search/Modal/index.tsx
Normal 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
|
||||
84
www/api-reference/components/Search/ModalOpener/index.tsx
Normal file
84
www/api-reference/components/Search/ModalOpener/index.tsx
Normal 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
|
||||
14
www/api-reference/components/Search/NoResults/index.tsx
Normal file
14
www/api-reference/components/Search/NoResults/index.tsx
Normal 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
|
||||
49
www/api-reference/components/Search/Suggestions/index.tsx
Normal file
49
www/api-reference/components/Search/Suggestions/index.tsx
Normal 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
|
||||
@@ -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
|
||||
135
www/api-reference/components/Select/Badge/index.tsx
Normal file
135
www/api-reference/components/Select/Badge/index.tsx
Normal 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
|
||||
151
www/api-reference/components/Select/Dropdown/index.tsx
Normal file
151
www/api-reference/components/Select/Dropdown/index.tsx
Normal 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
|
||||
127
www/api-reference/components/Select/Input/index.tsx
Normal file
127
www/api-reference/components/Select/Input/index.tsx
Normal 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
|
||||
9
www/api-reference/components/Select/types.ts
Normal file
9
www/api-reference/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">
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user