docs: create docs workspace (#5174)

* docs: migrate ui docs to docs universe

* created yarn workspace

* added eslint and tsconfig configurations

* fix eslint configurations

* fixed eslint configurations

* shared tailwind configurations

* added shared ui package

* added more shared components

* migrating more components

* made details components shared

* move InlineCode component

* moved InputText

* moved Loading component

* Moved Modal component

* moved Select components

* Moved Tooltip component

* moved Search components

* moved ColorMode provider

* Moved Notification components and providers

* used icons package

* use UI colors in api-reference

* moved Navbar component

* used Navbar and Search in UI docs

* added Feedback to UI docs

* general enhancements

* fix color mode

* added copy colors file from ui-preset

* added features and enhancements to UI docs

* move Sidebar component and provider

* general fixes and preparations for deployment

* update docusaurus version

* adjusted versions

* fix output directory

* remove rootDirectory property

* fix yarn.lock

* moved code component

* added vale for all docs MD and MDX

* fix tests

* fix vale error

* fix deployment errors

* change ignore commands

* add output directory

* fix docs test

* general fixes

* content fixes

* fix announcement script

* added changeset

* fix vale checks

* added nofilter option

* fix vale error
This commit is contained in:
Shahed Nasser
2023-09-21 20:57:15 +03:00
committed by GitHub
parent 19c5d5ba36
commit fa7c94b4cc
3209 changed files with 32188 additions and 31018 deletions
@@ -0,0 +1,53 @@
import React from "react"
import clsx from "clsx"
export type BadgeVariant =
| "purple"
| "purple-dark"
| "orange"
| "orange-dark"
| "green"
| "green-dark"
| "blue"
| "blue-dark"
| "red"
| "neutral"
export type BadgeProps = {
className?: string
variant: BadgeVariant
} & React.HTMLAttributes<HTMLSpanElement>
export const Badge = ({ className, variant, children }: BadgeProps) => {
return (
<span
className={clsx(
"text-compact-x-small-plus px-docs_0.4 rounded-docs_sm border border-solid py-px text-center",
variant === "purple" &&
"bg-medusa-tag-purple-bg dark:bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text dark:text-medusa-tag-purple-text-dark border-medusa-tag-purple-border dark:border-medusa-tag-purple-border-dark",
variant === "purple-dark" &&
"bg-medusa-tag-purple-bg-dark text-medusa-tag-purple-text-dark border-medusa-tag-purple-border-dark",
variant === "orange" &&
"bg-medusa-tag-orange-bg dark:bg-medusa-tag-orange-bg-dark text-medusa-tag-orange-text dark:text-medusa-tag-orange-text-dark border-medusa-tag-orange-border dark:border-medusa-tag-orange-border-dark",
variant === "orange-dark" &&
"bg-medusa-tag-orange-bg-dark text-medusa-tag-orange-text-dark border-medusa-tag-orange-border-dark",
variant === "green" &&
"bg-medusa-tag-green-bg dark:bg-medusa-tag-green-bg-dark text-medusa-tag-green-text dark:text-medusa-tag-green-text-dark border-medusa-tag-green-border dark:border-medusa-tag-green-border-dark",
variant === "green-dark" &&
"bg-medusa-tag-green-bg-dark text-medusa-tag-green-text-dark border-medusa-tag-green-border-dark",
variant === "blue" &&
"bg-medusa-tag-blue-bg dark:bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text dark:text-medusa-tag-blue-text-dark border-medusa-tag-blue-border dark:border-medusa-tag-blue-border-dark",
variant === "blue-dark" &&
"bg-medusa-tag-blue-bg-dark text-medusa-tag-blue-text-dark border-medusa-tag-blue-border-dark",
variant === "red" &&
"bg-medusa-tag-red-bg dark:bg-medusa-tag-red-bg-dark text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark border-medusa-tag-red-border dark:border-medusa-tag-red-border-dark",
variant === "neutral" &&
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"badge",
className
)}
>
{children}
</span>
)
}
@@ -0,0 +1,21 @@
import clsx from "clsx"
import React from "react"
export type BorderedProps = {
wrapperClassName?: string
} & React.HTMLAttributes<HTMLSpanElement>
export const Bordered = ({ wrapperClassName, children }: BorderedProps) => {
return (
<span
className={clsx(
"border-medusa-border-strong dark:border-medusa-border-strong-dark bg-docs-bg",
"dark:bg-docs-bg-dark mr-docs_1 inline-flex w-fit items-center justify-center rounded-docs_DEFAULT border border-solid p-[3px]",
"no-zoom-img",
wrapperClassName
)}
>
{children}
</span>
)
}
@@ -0,0 +1,51 @@
import React from "react"
import { Bordered } from "@/components/Bordered"
import clsx from "clsx"
import { IconProps } from "@medusajs/icons/dist/types"
export type BorderedIconProps = {
icon?: string
IconComponent?: React.FC<IconProps> | null
wrapperClassName?: string
iconWrapperClassName?: string
iconClassName?: string
iconColorClassName?: string
} & React.HTMLAttributes<HTMLSpanElement>
export const BorderedIcon = ({
icon = "",
IconComponent = null,
wrapperClassName,
iconWrapperClassName,
iconClassName,
iconColorClassName = "",
}: BorderedIconProps) => {
return (
<Bordered wrapperClassName={wrapperClassName}>
<span
className={clsx(
"rounded-docs_xs p-docs_0.125 bg-medusa-bg-component dark:bg-medusa-bg-component-dark inline-flex items-center justify-center",
iconWrapperClassName
)}
>
{!IconComponent && (
<img
src={icon || ""}
className={clsx(iconClassName, "bordered-icon")}
alt=""
/>
)}
{IconComponent && (
<IconComponent
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
iconClassName,
"bordered-icon",
iconColorClassName
)}
/>
)}
</span>
</Bordered>
)
}
@@ -0,0 +1,78 @@
import clsx from "clsx"
import React from "react"
export type ButtonVariants = "primary" | "secondary" | "clear"
export type ButtonType = "default" | "icon"
export type ButtonProps = {
isSelected?: boolean
disabled?: boolean
variant?: ButtonVariants
className?: string
buttonType?: ButtonType
buttonRef?: React.LegacyRef<HTMLButtonElement>
} & React.HTMLAttributes<HTMLButtonElement>
export const Button = ({
className,
children,
variant = "primary",
buttonType = "default",
buttonRef,
...props
}: ButtonProps) => {
const variantClasses = {
primary: [
"inline-flex flex-row justify-center items-center",
"py-[5px] px-docs_0.75 rounded-docs_sm cursor-pointer",
"bg-button-inverted bg-medusa-button-inverted dark:bg-button-inverted-dark dark:bg-medusa-button-inverted-dark",
"hover:bg-medusa-button-inverted-hover hover:bg-no-image dark:hover:bg-medusa-button-inverted-hover-dark hover:no-underline",
"active:bg-medusa-button-inverted-pressed active:bg-no-image dark:active:bg-medusa-button-inverted-pressed-dark",
"focus:bg-medusa-button-inverted-pressed focus:bg-no-image dark:focus:bg-medusa-button-inverted-pressed-dark",
"shadow-button-colored active:shadow-button-colored-focused focus:shadow-button-colored-focused transition-shadow",
"dark:shadow-button-colored-dark dark:active:shadow-button-colored-focused-dark dark:focus:shadow-button-colored-focused-dark",
"disabled:!bg-no-image disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:cursor-not-allowed disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
"text-compact-small-plus text-medusa-fg-on-inverted dark:text-medusa-fg-on-inverted-dark",
"[&_a]:text-medusa-fg-on-inverted dark:[&_a]:text-medusa-fg-on-inverted-dark",
"disabled:text-medusa-fg-disabled dark:disabled:text-medusa-fg-disabled-dark",
"[&_a]:disabled:text-medusa-fg-disabled dark:[&_a]:disabled:text-medusa-fg-disabled-dark",
"border border-medusa-border-loud dark:border-medusa-border-loud-dark",
"select-none",
],
secondary: [
"inline-flex flex-row justify-center items-center",
"py-[5px] px-docs_0.75 rounded-docs_sm cursor-pointer",
"bg-button-neutral bg-medusa-button-neutral dark:bg-button-neutral-dark dark:bg-medusa-button-neutral-dark",
"hover:bg-medusa-button-neutral-hover hover:bg-no-image dark:hover:bg-medusa-button-neutral-hover-dark hover:no-underline",
"active:bg-medusa-button-neutral-pressed active:bg-no-image dark:active:bg-medusa-button-neutral-pressed-dark",
"focus:bg-medusa-button-neutral-pressed focus:bg-no-image dark:focus:bg-medusa-button-neutral-pressed-dark",
"disabled:!bg-no-image disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:cursor-not-allowed",
"border border-solid border-medusa-border-base dark:border-medusa-border-base-dark",
"text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
"[&_a]:text-medusa-fg-base dark:[&_a]:text-medusa-fg-base-dark",
"shadow-button-neutral focus:shadow-button-neutral-focused active:shadow-button-neutral-focused transition-shadow",
"dark:shadow-button-neutral dark:focus:shadow-button-neutral-focused dark:active:shadow-button-neutral-focused",
"select-none",
],
clear: ["bg-transparent shadow-none border-0 outline-none cursor-pointer"],
}
return (
<button
className={clsx(
variant === "primary" && variantClasses.primary,
variant === "secondary" && variantClasses.secondary,
variant === "clear" && variantClasses.clear,
buttonType === "icon" && "!px-docs_0.25",
className
)}
ref={buttonRef}
{...props}
>
{children}
</button>
)
}
@@ -0,0 +1,50 @@
import React from "react"
import { ArrowUpRightOnBox } from "@medusajs/icons"
import clsx from "clsx"
import { Link } from "@/components"
export type CardProps = {
icon?: React.ReactNode
title: string
text?: string
href?: string
className?: string
}
export const Card = ({ icon, title, text, href, className }: CardProps) => {
return (
<div
className={clsx(
"bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark w-full rounded",
"shadow-card-rest dark:shadow-card-rest-dark py-docs_0.75 relative px-docs_1",
"flex items-center gap-docs_1 transition-shadow",
href && "hover:shadow-card-hover dark:hover:shadow-card-hover-dark",
className
)}
>
{icon}
<div className="flex items-center gap-docs_1 justify-between flex-1">
<div className="flex flex-col">
<span className="text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark">
{title}
</span>
{text && (
<span className="text-compact-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
{text}
</span>
)}
</div>
{href && (
<>
<ArrowUpRightOnBox className="text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark" />
<Link
href={href}
className="absolute left-0 top-0 h-full w-full rounded"
/>
</>
)}
</div>
</div>
)
}
@@ -0,0 +1,114 @@
"use client"
import React from "react"
import clsx from "clsx"
import { HighlightProps, Highlight, themes } from "prism-react-renderer"
import { CopyButton, useColorMode } from "docs-ui"
import { SquareTwoStackSolid } from "@medusajs/icons"
export type CodeBlockProps = {
source: string
lang?: string
className?: string
collapsed?: boolean
} & Omit<HighlightProps, "code" | "language" | "children">
export const CodeBlock = ({
source,
lang = "",
className,
collapsed = false,
...rest
}: CodeBlockProps) => {
const { colorMode } = useColorMode()
return (
<div
className={clsx(
"bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark relative mb-docs_1 rounded-docs_DEFAULT",
"border-medusa-code-border dark:border-medusa-code-border-dark w-full max-w-full border",
"xs:after:content-[''] xs:after:rounded xs:after:absolute xs:after:right-0 xs:after:top-0 xs:after:w-[calc(10%+24px)] xs:after:h-full xs:after:bg-code-fade xs:dark:after:bg-code-fade-dark",
collapsed && "max-h-[400px] overflow-auto",
className
)}
>
<Highlight
theme={{
...themes.vsDark,
plain: {
...themes.vsDark.plain,
backgroundColor: colorMode === "light" ? "#111827" : "#1B1B1F",
},
}}
code={source.trim()}
language={lang}
{...rest}
>
{({
className: preClassName,
style,
tokens,
getLineProps,
getTokenProps,
}) => (
<>
<pre
style={{ ...style, fontStretch: "100%" }}
className={clsx(
"xs:max-w-[90%] relative !mt-0 break-words bg-transparent !outline-none",
"overflow-auto break-words rounded-docs_DEFAULT",
preClassName
)}
>
<code
className={clsx(
"text-code-body font-monospace table min-w-full pb-docs_1.5 print:whitespace-pre-wrap",
tokens.length > 1 && "pt-docs_1 pr-docs_1",
tokens.length <= 1 && "py-docs_0.5 px-docs_1"
)}
>
{tokens.map((line, i) => {
const lineProps = getLineProps({ line })
return (
<span
key={i}
{...lineProps}
className={clsx("table-row", lineProps.className)}
>
{tokens.length > 1 && (
<span
className={clsx(
"text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark mr-docs_1 table-cell select-none",
"bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark sticky left-0 w-[1%] px-docs_1 text-right"
)}
>
{i + 1}
</span>
)}
<span>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</span>
</span>
)
})}
</code>
</pre>
<div
className={clsx(
"absolute z-50 hidden gap-docs_1 md:flex",
tokens.length === 1 && "right-docs_0.75 top-[10px]",
tokens.length > 1 && "right-docs_1 top-docs_1"
)}
>
<CopyButton text={source} tooltipClassName="font-base">
<SquareTwoStackSolid className="text-medusa-code-icon dark:text-medusa-code-icon-dark" />
</CopyButton>
</div>
</>
)}
</Highlight>
</div>
)
}
@@ -0,0 +1,25 @@
import React from "react"
import { CodeBlock, InlineCode } from "@/components"
export type CodeMdxProps = {
className?: string
children?: React.ReactNode
}
// due to how mdx handles code blocks
// it is required that a code block specify a language
// to be considered a block. Otherwise, it will be
// considered as inline code
export const CodeMdx = ({ className, children }: CodeMdxProps) => {
if (!children) {
return <></>
}
const match = /language-(\w+)/.exec(className || "")
if (match) {
return <CodeBlock source={children as string} lang={match[1]} />
}
return <InlineCode>{children}</InlineCode>
}
@@ -0,0 +1,129 @@
"use client"
import React, { useCallback, useEffect, useMemo, useRef } from "react"
import clsx from "clsx"
import { CodeBlock, CodeBlockProps } from "@/components"
import { useTabs, BaseTabType, useScrollPositionBlocker } from "@/hooks"
export type TabType = {
code?: CodeBlockProps
codeBlock?: React.ReactNode
} & BaseTabType
export type CodeTabsProps = {
tabs: TabType[]
className?: string
group?: string
}
export const CodeTabs = ({
tabs,
className,
group = "client",
}: CodeTabsProps) => {
const { selectedTab, changeSelectedTab } = useTabs<TabType>({
tabs,
group,
})
const tabRefs: (HTMLButtonElement | null)[] = useMemo(() => [], [])
const codeTabSelectorRef = useRef<HTMLSpanElement | null>(null)
const codeTabsWrapperRef = useRef<HTMLDivElement | null>(null)
const { blockElementScrollPositionUntilNextRender } =
useScrollPositionBlocker()
const changeTabSelectorCoordinates = useCallback(
(selectedTabElm: HTMLElement) => {
if (!codeTabSelectorRef?.current || !codeTabsWrapperRef?.current) {
return
}
const selectedTabsCoordinates = selectedTabElm.getBoundingClientRect()
const tabsWrapperCoordinates =
codeTabsWrapperRef.current.getBoundingClientRect()
codeTabSelectorRef.current.style.left = `${
selectedTabsCoordinates.left - tabsWrapperCoordinates.left
}px`
codeTabSelectorRef.current.style.width = `${selectedTabsCoordinates.width}px`
codeTabSelectorRef.current.style.height = `${selectedTabsCoordinates.height}px`
},
[]
)
useEffect(() => {
if (codeTabSelectorRef?.current && tabRefs.length) {
const selectedTabElm = tabRefs.find(
(tab) => tab?.getAttribute("aria-selected") === "true"
)
if (selectedTabElm) {
changeTabSelectorCoordinates(
selectedTabElm.parentElement || selectedTabElm
)
}
}
}, [codeTabSelectorRef, tabRefs, changeTabSelectorCoordinates, selectedTab])
return (
<div
className={clsx(
"relative my-docs_1 w-full max-w-full overflow-auto",
className
)}
ref={codeTabsWrapperRef}
>
<span
className={clsx(
"xs:absolute xs:border xs:border-solid xs:border-medusa-code-border dark:xs:border-medusa-code-border-dark xs:bg-medusa-code-bg-base dark:xs:bg-medusa-code-bg-base-dark ",
"xs:transition-all xs:duration-200 xs:ease-ease xs:top-[13px] xs:z-[1] xs:rounded-full"
)}
ref={codeTabSelectorRef}
></span>
<ul
className={clsx(
"bg-medusa-code-bg-header dark:bg-medusa-code-bg-header-dark py-docs_0.75 flex !list-none rounded-t-docs_DEFAULT px-docs_1",
"border-medusa-code-border dark:border-medusa-code-border-dark border border-b-0",
"gap-docs_0.25 mb-0"
)}
>
{tabs.map((tab, index) => (
<li key={index}>
<button
className={clsx(
"text-compact-small-plus xs:border-0 py-docs_0.25 px-docs_0.75 relative z-[2] rounded-full border",
(!selectedTab || selectedTab.value !== tab.value) && [
"text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark border-transparent",
"hover:bg-medusa-code-bg-base dark:hover:bg-medusa-code-bg-base-dark",
],
selectedTab?.value === tab.value && [
"text-medusa-code-text-base bg-medusa-code-bg-base xs:!bg-transparent",
"dark:text-medusa-code-text-base-dark dark:bg-medusa-code-bg-base-dark xs:!bg-transparent",
]
)}
ref={(tabControl) => tabRefs.push(tabControl)}
onClick={(e) => {
blockElementScrollPositionUntilNextRender(
e.target as HTMLButtonElement
)
changeSelectedTab(tab)
}}
aria-selected={selectedTab?.value === tab.value}
role="tab"
>
{tab.label}
</button>
</li>
))}
</ul>
<>
{selectedTab?.code && (
<CodeBlock
{...selectedTab?.code}
className={clsx(
"!mt-0 !rounded-t-none",
selectedTab.code.className
)}
/>
)}
{selectedTab?.codeBlock && <>{selectedTab.codeBlock}</>}
</>
</div>
)
}
@@ -0,0 +1,46 @@
"use client"
import React, { useState, useEffect, useRef, useCallback } from "react"
import copy from "copy-text-to-clipboard"
import clsx from "clsx"
import { Tooltip } from "@/components/Tooltip"
export type CopyButtonProps = {
text: string
buttonClassName?: string
tooltipClassName?: string
} & React.HTMLAttributes<HTMLDivElement>
export const CopyButton = ({
text,
buttonClassName = "",
tooltipClassName = "",
children,
}: CopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false)
const copyTimeout = useRef<number>(0)
const handleCopy = useCallback(() => {
copy(text)
setIsCopied(true)
copyTimeout.current = window.setTimeout(() => {
setIsCopied(false)
}, 1000)
}, [text])
useEffect(() => () => window.clearTimeout(copyTimeout.current), [])
return (
<Tooltip
text={isCopied ? `Copied!` : `Copy to Clipboard`}
tooltipClassName={tooltipClassName}
>
<span
className={clsx("cursor-pointer", buttonClassName)}
onClick={handleCopy}
>
{children}
</span>
</Tooltip>
)
}
@@ -0,0 +1,64 @@
import React from "react"
import clsx from "clsx"
import { PlusMini } from "@medusajs/icons"
export type DetailsSummaryProps = {
title: React.ReactNode
subtitle?: string
badge?: React.ReactNode
expandable?: boolean
open?: boolean
className?: string
titleClassName?: string
} & Omit<React.HTMLAttributes<HTMLElement>, "title">
export const DetailsSummary = ({
title,
subtitle,
badge,
expandable = true,
open = false,
className,
titleClassName,
...rest
}: DetailsSummaryProps) => {
return (
<summary
className={clsx(
"py-docs_0.75 flex items-center justify-between",
expandable && "cursor-pointer",
!expandable &&
"border-medusa-border-base dark:border-medusa-border-base-dark border-y",
"no-marker",
className
)}
{...rest}
>
<span className="gap-docs_0.25 flex flex-col">
<span
className={clsx(
"text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
titleClassName
)}
>
{title}
</span>
{subtitle && (
<span className="text-compact-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
{subtitle}
</span>
)}
</span>
{(badge || expandable) && (
<span className="flex gap-docs_0.5">
{badge}
{expandable && (
<PlusMini
className={clsx("transition-transform", open && "rotate-45")}
/>
)}
</span>
)}
</summary>
)
}
@@ -0,0 +1,93 @@
"use client"
import React, { Suspense, cloneElement, useRef, useState } from "react"
import { Loading } from "@/components/Loading"
import clsx from "clsx"
import { CSSTransition } from "react-transition-group"
import { DetailsSummary } from "./Summary"
export type DetailsProps = {
openInitial?: boolean
summaryContent?: React.ReactNode
summaryElm?: React.ReactNode
} & React.HTMLAttributes<HTMLDetailsElement>
export const Details = ({
openInitial = false,
summaryContent,
summaryElm,
children,
...props
}: DetailsProps) => {
const [open, setOpen] = useState(openInitial)
const [showContent, setShowContent] = useState(openInitial)
const ref = useRef<HTMLDetailsElement>(null)
const handleToggle = () => {
if (open) {
setShowContent(false)
} else {
setOpen(true)
setShowContent(true)
}
}
return (
<details
{...props}
ref={ref}
open={open}
onClick={(event) => {
event.preventDefault()
}}
onToggle={(event) => {
// this is to avoid event propagation
// when details are nested, which is a bug
// in react. Learn more here:
// https://github.com/facebook/react/issues/22718
event.stopPropagation()
}}
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark border-y border-solid border-x-0",
"overflow-hidden [&>summary]:relative [&>summary]:z-[398]",
props.className
)}
>
{summaryContent && (
<DetailsSummary
onClick={handleToggle}
className="cursor-pointer"
title={summaryContent}
/>
)}
{summaryElm &&
cloneElement(summaryElm as React.ReactElement, {
open,
onClick: handleToggle,
})}
<CSSTransition
unmountOnExit
in={showContent}
timeout={150}
onEnter={(node: HTMLElement) => {
node.classList.add(
"!mb-docs_2",
"!mt-0",
"translate-y-docs_1",
"transition-transform"
)
}}
onExit={(node: HTMLElement) => {
node.classList.add("transition-transform", "!-translate-y-docs_1")
setTimeout(() => {
setOpen(false)
}, 100)
}}
>
<Suspense fallback={<Loading className="!mb-docs_2 !mt-0" />}>
{children}
</Suspense>
</CSSTransition>
</details>
)
}
@@ -0,0 +1,92 @@
"use client"
import React, { useEffect, useState } from "react"
import { request } from "@octokit/request"
import { Link } from "@/components/Link"
export type SolutionsProps = {
feedback: boolean
message?: string
}
export type GitHubSearchItem = {
url: string
html_url: string
title: string
[key: string]: unknown
}
export const Solutions = ({ feedback, message }: SolutionsProps) => {
const [possibleSolutionsQuery, setPossibleSolutionsQuery] =
useState<string>("")
const [possibleSolutions, setPossibleSolutions] = useState<
GitHubSearchItem[]
>([])
function constructQuery(searchQuery: string) {
return `${searchQuery} repo:medusajs/medusa is:closed is:issue`
}
async function searchGitHub(query: string) {
return request(`GET /search/issues`, {
q: query,
sort: "updated",
per_page: 3,
})
}
useEffect(() => {
if (!feedback) {
let query = constructQuery(
// Github does not allow queries longer than 256 characters
message ? message.substring(0, 256) : document.title
)
searchGitHub(query)
.then(async (result) => {
if (!result.data.items.length && message) {
query = constructQuery(document.title)
result = await searchGitHub(query)
}
setPossibleSolutionsQuery(query)
setPossibleSolutions(result.data.items)
})
.catch((err) => console.error(err))
} else {
setPossibleSolutionsQuery("")
setPossibleSolutions([])
}
}, [feedback, message])
return (
<>
{possibleSolutions.length > 0 && (
<div className="text-compact-large-plus font-normal">
<span className="my-docs_1 mx-0 inline-block">
If you faced a problem, here are some possible solutions from
GitHub:
</span>
<ul>
{possibleSolutions.map((solution) => (
<li key={solution.url} className="mb-docs_0.5 last:mb-0">
<Link href={solution.html_url} target="_blank" rel="noreferrer">
{solution.title}
</Link>
</li>
))}
</ul>
<span>
Explore more issues in{" "}
<a
href={`https://github.com/medusajs/medusa/issues?q=${possibleSolutionsQuery}`}
target="_blank"
rel="noreferrer"
>
the GitHub repository
</a>
</span>
</div>
)}
</>
)
}
@@ -0,0 +1,264 @@
"use client"
import React, { useRef, useState } from "react"
import { CSSTransition, SwitchTransition } from "react-transition-group"
import { Solutions } from "./Solutions"
import { ExtraData, useAnalytics } from "@/providers/Analytics"
import clsx from "clsx"
import { TextArea } from "@/components/TextArea"
import { Label } from "@/components/Label"
import { Button } from "docs-ui"
import { Details } from "@/components/Details"
import { InputText } from "@/components/Input/Text"
export type FeedbackProps = {
event: string
pathName: string
reportLink?: string
question?: string
positiveBtn?: string
negativeBtn?: string
positiveQuestion?: string
negativeQuestion?: string
submitBtn?: string
submitMessage?: string
showPossibleSolutions?: boolean
className?: string
extraData?: ExtraData
vertical?: boolean
showLongForm?: boolean
} & React.HTMLAttributes<HTMLDivElement>
export const Feedback = ({
event,
pathName,
reportLink,
question = "Was this section helpful?",
positiveBtn = "Yes",
negativeBtn = "No",
positiveQuestion = "What was most helpful?",
negativeQuestion = "What can we improve?",
submitBtn = "Submit",
submitMessage = "Thank you for helping improve our documentation!",
showPossibleSolutions = true,
className = "",
extraData = {},
vertical = false,
showLongForm = false,
}: FeedbackProps) => {
const [showForm, setShowForm] = useState(false)
const [submittedFeedback, setSubmittedFeedback] = useState(false)
const [loading, setLoading] = useState(false)
const inlineFeedbackRef = useRef<HTMLDivElement>(null)
const inlineQuestionRef = useRef<HTMLDivElement>(null)
const inlineMessageRef = useRef<HTMLDivElement>(null)
const [positiveFeedback, setPositiveFeedback] = useState(false)
const [message, setMessage] = useState("")
const [steps, setSteps] = useState("")
const [medusaVersion, setMedusaVersion] = useState("")
const [errorFix, setErrorFix] = useState("")
const [contactInfo, setContactInfo] = useState("")
const nodeRef: React.RefObject<HTMLDivElement> = submittedFeedback
? inlineMessageRef
: showForm
? inlineQuestionRef
: inlineFeedbackRef
const { loaded, track } = useAnalytics()
function handleFeedback(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (!loaded) {
return
}
const feedback = (e.target as Element).classList.contains("positive")
setPositiveFeedback(feedback)
setShowForm(true)
submitFeedback(e, feedback)
}
function submitFeedback(
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
feedback = false
) {
if (showForm) {
setLoading(true)
}
track(
event,
{
url: pathName,
label: document.title,
feedback:
(feedback !== null && feedback) ||
(feedback === null && positiveFeedback)
? "yes"
: "no",
message: message?.length ? message : null,
os: window.navigator.userAgent,
...extraData,
},
function () {
if (showForm) {
setLoading(false)
resetForm()
}
}
)
}
function resetForm() {
setShowForm(false)
setSubmittedFeedback(true)
}
return (
<div className={clsx("mt-docs_3", className)}>
<SwitchTransition mode="out-in">
<CSSTransition
key={
showForm
? "show_form"
: !submittedFeedback
? "feedback"
: "submitted_feedback"
}
nodeRef={nodeRef}
timeout={300}
addEndListener={(done) => {
nodeRef.current?.addEventListener("transitionend", done, false)
}}
classNames={{
enter: "animate-fadeIn animation-fill-forwards animate-fast",
exit: "animate-fadeOut animation-fill-forwards animate-fast",
}}
>
<>
{!showForm && !submittedFeedback && (
<div
className={clsx(
"flex",
!vertical && "flex-row items-center",
vertical && "flex-col justify-center gap-docs_1"
)}
ref={inlineFeedbackRef}
>
<Label className="mr-docs_1.5">{question}</Label>
<div
className={clsx("flex flex-row items-center gap-docs_0.5")}
>
<Button
onClick={handleFeedback}
className="positive w-fit"
variant="secondary"
>
{positiveBtn}
</Button>
<Button
onClick={handleFeedback}
className="w-fit"
variant="secondary"
>
{negativeBtn}
</Button>
{reportLink && (
<Button variant="secondary">
<a href={reportLink}>Report Issue</a>
</Button>
)}
</div>
</div>
)}
{showForm && !submittedFeedback && (
<div className="flex flex-col gap-docs_1" ref={inlineQuestionRef}>
<Label>
{positiveFeedback ? positiveQuestion : negativeQuestion}
</Label>
<TextArea
rows={4}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
{showLongForm && !positiveFeedback && (
<Details summaryContent="More Details" className="mt-docs_1">
<div className="flex flex-col gap-docs_0.5">
<div className="flex flex-col gap-docs_0.5">
<Label>
Can you provide the exact steps you took before
receiving the error? For example, the commands you
ran.
</Label>
<TextArea
rows={4}
value={steps}
onChange={(e) => setSteps(e.target.value)}
placeholder="1. I ran npm dev..."
/>
</div>
<div className="flex flex-col gap-docs_0.5">
<Label>
If applicable, what version of Medusa are you using?
If a plugin is related to the error, please provide a
version of that as well.
</Label>
<TextArea
rows={4}
value={medusaVersion}
onChange={(e) => setMedusaVersion(e.target.value)}
placeholder="@medusajs/medusa: vX"
/>
</div>
<div className="flex flex-col gap-docs_0.5">
<Label>
Were you able to fix the error? If so, what steps did
you follow?
</Label>
<TextArea
rows={4}
value={errorFix}
onChange={(e) => setErrorFix(e.target.value)}
placeholder="@medusajs/medusa: vX"
/>
</div>
<div className="flex flex-col gap-docs_0.5">
<Label>
Can you provide your email or discord username? This
would allow us to contact you for further info or
assist you with your issue.
</Label>
<InputText
value={contactInfo}
onChange={(e) => setContactInfo(e.target.value)}
placeholder="user@example.com"
/>
</div>
</div>
</Details>
)}
<Button
onClick={submitFeedback}
disabled={loading}
className="w-fit"
variant="secondary"
>
{submitBtn}
</Button>
</div>
)}
{submittedFeedback && (
<div>
<div
className="text-compact-large-plus flex flex-col"
ref={inlineMessageRef}
>
<span>{submitMessage}</span>
{showPossibleSolutions && (
<Solutions message={message} feedback={positiveFeedback} />
)}
</div>
</div>
)}
</>
</CSSTransition>
</SwitchTransition>
</div>
)
}
@@ -0,0 +1,31 @@
"use client"
import React from "react"
import clsx from "clsx"
import { CopyButton } from "@/components"
export type InlineCodeProps = React.ComponentProps<"code">
export const InlineCode = (props: InlineCodeProps) => {
return (
<CopyButton
text={props.children as string}
buttonClassName={clsx(
"bg-transparent border-0 p-0 inline text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"active:[&>code]:bg-medusa-bg-subtle-pressed dark:active:[&>code]:bg-medusa-bg-subtle-pressed-dark",
"focus:[&>code]:bg-medusa-bg-subtle-pressed dark:focus:[&>code]:bg-medusa-bg-subtle-pressed-dark",
"hover:[&>code]:bg-medusa-bg-subtle-hover dark:hover:[&>code]:bg-medusa-bg-base-hover-dark"
)}
>
<code
{...props}
className={clsx(
"border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark border",
"text-medusa-tag-neutral-text dark:text-medusa-tag-neutral-text-dark",
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark font-monospace text-code-label rounded-docs_sm py-0 px-[6px]",
props.className
)}
/>
</CopyButton>
)
}
@@ -0,0 +1,31 @@
import React from "react"
import clsx from "clsx"
export type InputTextProps = {
className?: string
} & React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>
export const InputText = (props: InputTextProps) => {
return (
<input
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-docs_sm border border-solid",
"px-docs_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",
"active:border-medusa-border-interactive dark:active:border-medusa-border-interactive-dark",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
"disabled:placeholder:text-medusa-fg-disabled dark:disabled:placeholder:text-medusa-fg-disabled-dark",
"text-compact-medium font-base",
props.className
)}
/>
)
}
@@ -0,0 +1,23 @@
import React from "react"
import clsx from "clsx"
export type KbdProps = React.ComponentProps<"kbd">
export const Kbd = ({ children, className, ...props }: KbdProps) => {
return (
<kbd
className={clsx(
"rounded-docs_sm border-solid py-0 px-[6px]",
"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 font-monospace shadow-none",
className
)}
{...props}
>
{children}
</kbd>
)
}
@@ -0,0 +1,19 @@
import clsx from "clsx"
import React from "react"
export type LabelProps = {
className?: string
} & React.HTMLAttributes<HTMLSpanElement>
export const Label = ({ children, className }: LabelProps) => {
return (
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium-plus",
className
)}
>
{children}
</span>
)
}
@@ -0,0 +1,24 @@
import React from "react"
import clsx from "clsx"
export type LinkProps = {
href?: string
children?: React.ReactNode
className?: string
} & React.AllHTMLAttributes<HTMLAnchorElement>
export const Link = ({ href, children, className, ...rest }: LinkProps) => {
return (
<a
href={href || ""}
{...rest}
className={clsx(
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
"dark:text-medusa-fg-interactive-dark dark:hover:text-medusa-fg-interactive-hover-dark",
className
)}
>
{children}
</a>
)
}
@@ -0,0 +1,19 @@
import React from "react"
import clsx from "clsx"
import { Spinner } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
export type SpinnerLoadingProps = {
iconProps?: IconProps
}
export const SpinnerLoading = ({ iconProps }: SpinnerLoadingProps) => {
return (
<span role="status">
<Spinner
{...iconProps}
className={clsx("animate-spin", iconProps?.className)}
/>
</span>
)
}
@@ -0,0 +1,43 @@
import React from "react"
import clsx from "clsx"
export type LoadingProps = {
className?: string
barClassName?: string
count?: number
}
export const Loading = ({
className,
count = 6,
barClassName,
}: LoadingProps) => {
const getLoadingBars = () => {
const bars = []
for (let i = 0; i < count; i++) {
bars.push(
<span
className={clsx(
"bg-medusa-bg-subtle-pressed dark:bg-medusa-bg-subtle-pressed-dark h-docs_1 w-full rounded-full",
barClassName
)}
key={i}
></span>
)
}
return bars
}
return (
<span
role="status"
className={clsx(
"my-docs_1 flex w-full animate-pulse flex-col gap-docs_1",
className
)}
>
{getLoadingBars()}
<span className="sr-only">Loading...</span>
</span>
)
}
@@ -0,0 +1,31 @@
import React from "react"
import clsx from "clsx"
import { Button, ButtonProps } from "@/components"
export type ModalFooterProps = {
actions?: ButtonProps[]
children?: React.ReactNode
className?: string
}
export const ModalFooter = ({
actions,
children,
className,
}: ModalFooterProps) => {
return (
<div
className={clsx(
"py-docs_1.5 pl-0 pr-docs_2",
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-t border-solid",
"flex justify-end gap-docs_0.5",
className
)}
>
{actions?.map((action, index) => (
<Button {...action} key={index} />
))}
{children}
</div>
)
}
@@ -0,0 +1,37 @@
import React from "react"
import clsx from "clsx"
import { useModal } from "@/providers"
import { Button } from "@/components"
import { XMark } from "@medusajs/icons"
export type ModalHeaderProps = {
title?: string
}
export const ModalHeader = ({ title }: ModalHeaderProps) => {
const { closeModal } = useModal()
return (
<div
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-b border-solid py-docs_1.5 px-docs_2",
"flex items-center justify-between"
)}
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-h2"
)}
>
{title}
</span>
<Button
variant="clear"
className="cursor-pointer"
onClick={() => closeModal()}
>
<XMark />
</Button>
</div>
)
}
@@ -0,0 +1,122 @@
"use client"
import clsx from "clsx"
import React, { useCallback, useEffect, useRef } from "react"
import { useModal } from "@/providers"
import { ModalHeader } from "./Header"
import { ModalFooter } from "./Footer"
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"
import { ButtonProps } from "@/components"
import { Ref } from "@/types"
export type ModalProps = {
className?: string
title?: string
actions?: ButtonProps[]
modalContainerClassName?: string
contentClassName?: string
onClose?: React.ReactEventHandler<HTMLDialogElement>
open?: boolean
footerContent?: React.ReactNode
passedRef?: Ref<HTMLDialogElement>
} & Omit<React.ComponentProps<"dialog">, "ref">
export const Modal = ({
className,
title,
actions,
children,
contentClassName,
modalContainerClassName,
onClose,
open = true,
footerContent,
passedRef,
...props
}: ModalProps) => {
const { closeModal } = useModal()
const ref = useRef<HTMLDialogElement | null>(null)
const setRefs = useCallback(
(node: HTMLDialogElement) => {
// Ref's from useRef needs to have the node assigned to `current`
ref.current = node
if (typeof passedRef === "function") {
passedRef(node)
} else if (passedRef && "current" in passedRef) {
passedRef.current = node
}
},
[passedRef]
)
useKeyboardShortcut({
metakey: false,
checkEditing: false,
shortcutKeys: ["escape"],
action: () => {
if (open) {
ref.current?.close()
}
},
})
const handleClick = (e: React.MouseEvent<HTMLDialogElement, MouseEvent>) => {
// close modal when the user clicks outside the content
if (e.target === ref.current) {
closeModal()
onClose?.(e)
}
}
const handleClose = (e: React.SyntheticEvent<HTMLDialogElement, Event>) => {
onClose?.(e)
closeModal()
}
useEffect(() => {
if (open) {
document.body.setAttribute("data-modal", "opened")
} else {
document.body.removeAttribute("data-modal")
}
}, [open])
return (
<dialog
{...props}
className={clsx(
"fixed top-0 left-0 flex h-screen w-screen items-center justify-center",
"bg-medusa-bg-overlay dark:bg-medusa-bg-overlay-dark z-[500]",
"hidden open:flex border-0 p-0",
className
)}
onClick={handleClick}
ref={setRefs}
onClose={handleClose}
open={open}
>
<div
className={clsx(
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded-docs_sm",
"border-medusa-border-base dark:border-medusa-border-base-dark border border-solid",
"shadow-modal dark:shadow-modal-dark",
"w-[90%] md:h-auto md:w-[75%] lg:w-[560px]",
modalContainerClassName
)}
>
{title && <ModalHeader title={title} />}
<div
className={clsx(
"overflow-auto py-docs_1.5 px-docs_2",
contentClassName
)}
>
{children}
</div>
{actions && actions?.length > 0 && <ModalFooter actions={actions} />}
{footerContent && <ModalFooter>{footerContent}</ModalFooter>}
</div>
</dialog>
)
}
@@ -0,0 +1,27 @@
"use client"
import React from "react"
import { NavbarIconButton, NavbarIconButtonProps } from "../IconButton"
import { useColorMode } from "@/providers"
import { Moon, Sun } from "@medusajs/icons"
export type NavbarColorModeToggleProps = {
buttonProps?: NavbarIconButtonProps
}
export const NavbarColorModeToggle = ({
buttonProps,
}: NavbarColorModeToggleProps) => {
const { colorMode, toggleColorMode } = useColorMode()
return (
<NavbarIconButton {...buttonProps} onClick={() => toggleColorMode()}>
{colorMode === "light" && (
<Sun className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
{colorMode === "dark" && (
<Moon className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
</NavbarIconButton>
)
}
@@ -0,0 +1,24 @@
import React from "react"
import clsx from "clsx"
import { Button, ButtonProps } from "@/components"
export type NavbarIconButtonProps = ButtonProps
export const NavbarIconButton = ({
children,
className,
...props
}: NavbarIconButtonProps) => {
return (
<Button
className={clsx(
"[&>svg]:h-[22px] [&>svg]:w-[22px] btn-secondary-icon",
className
)}
variant="secondary"
{...props}
>
{children}
</Button>
)
}
@@ -0,0 +1,45 @@
"use client"
import React, { useMemo } from "react"
import clsx from "clsx"
import { useNavbar } from "@/providers"
import { NextLink, NextLinkProps } from "@/components"
export type NavbarLinkProps = {
href: string
label: string
className?: string
activeValuePattern?: RegExp
} & NextLinkProps
export const NavbarLink = ({
href,
label,
className,
activeValuePattern,
}: NavbarLinkProps) => {
const { activeItem } = useNavbar()
const isActive = useMemo(() => {
return activeItem
? activeValuePattern
? activeValuePattern.test(activeItem)
: href === activeItem
: false
}, [activeItem, href, activeValuePattern])
return (
<NextLink
href={href}
className={clsx(
isActive && "!text-medusa-fg-base dark:!text-medusa-fg-base-dark",
!isActive && "!text-medusa-fg-subtle dark:!text-medusa-fg-subtle-dark",
"text-compact-small-plus inline-block",
"hover:!text-medusa-fg-base dark:hover:!text-medusa-fg-base-dark",
className
)}
>
{label}
</NextLink>
)
}
@@ -0,0 +1,34 @@
"use client"
import React from "react"
import { useColorMode } from "@/providers"
import Link from "next/link"
import clsx from "clsx"
export type NavbarLogoProps = {
light: string
dark?: string
className?: string
imageClassName?: string
}
export const NavbarLogo = ({
light,
dark,
className,
imageClassName,
}: NavbarLogoProps) => {
const { colorMode } = useColorMode()
return (
<Link href={`/`} className={clsx("flex-1", className)}>
<img
src={colorMode === "light" ? light : dark || light}
alt="Medusa Logo"
height={20}
width={20}
className={clsx("align-middle", imageClassName)}
/>
</Link>
)
}
@@ -0,0 +1,39 @@
"use client"
import React from "react"
import { NavbarIconButton, NavbarIconButtonProps } from "../../IconButton"
import clsx from "clsx"
import { Sidebar, XMark } from "@medusajs/icons"
export type NavbarMobileMenuButtonProps = {
buttonProps?: NavbarIconButtonProps
mobileSidebarOpen: boolean
setMobileSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
isLoading?: boolean
}
export const NavbarMobileMenuButton = ({
buttonProps,
mobileSidebarOpen,
setMobileSidebarOpen,
isLoading = false,
}: NavbarMobileMenuButtonProps) => {
return (
<NavbarIconButton
{...buttonProps}
className={clsx("mr-docs_1 lg:!hidden", buttonProps?.className)}
onClick={() => {
if (!isLoading) {
setMobileSidebarOpen((prevValue) => !prevValue)
}
}}
>
{!mobileSidebarOpen && (
<Sidebar className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
{mobileSidebarOpen && (
<XMark className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
</NavbarIconButton>
)
}
@@ -0,0 +1,54 @@
"use client"
import React from "react"
import { NavbarMobileMenuButton, NavbarMobileMenuButtonProps } from "./Button"
import { NavbarColorModeToggle } from "../ColorModeToggle"
import { NavbarSearchModalOpener } from "../SearchModalOpener"
import { useMobile } from "@/providers"
import clsx from "clsx"
import { NavbarLogo, NavbarLogoProps } from "../Logo"
export type NavbarMobileMenuProps = {
menuButton: NavbarMobileMenuButtonProps
logo: NavbarLogoProps
}
export const NavbarMobileMenu = ({
menuButton,
logo,
}: NavbarMobileMenuProps) => {
const { isMobile } = useMobile()
return (
<div className="flex w-full items-center justify-between lg:hidden">
{isMobile && (
<>
<NavbarMobileMenuButton
{...menuButton}
buttonProps={{
...(menuButton.buttonProps || {}),
className: clsx(
menuButton.buttonProps?.className,
"!border-none !bg-transparent !bg-no-image !shadow-none"
),
}}
/>
<NavbarLogo
{...logo}
className="lg:hidden"
imageClassName="mx-auto"
/>
<div className="flex">
<NavbarSearchModalOpener />
<NavbarColorModeToggle
buttonProps={{
className:
"!border-none !bg-transparent !bg-no-image !shadow-none ml-docs_1",
}}
/>
</div>
</>
)}
</div>
)
}
@@ -0,0 +1,17 @@
"use client"
import React from "react"
import { SearchModalOpener } from "@/components"
import { useMobile } from "@/providers"
export type NavbarSearchModalOpenerProps = {
isLoading?: boolean
}
export const NavbarSearchModalOpener = ({
isLoading,
}: NavbarSearchModalOpenerProps) => {
const { isMobile } = useMobile()
return <SearchModalOpener isMobile={isMobile} isLoading={isLoading} />
}
@@ -0,0 +1,67 @@
import React from "react"
import clsx from "clsx"
import { NavbarLink, NavbarLinkProps } from "./Link"
import { NavbarColorModeToggle } from "./ColorModeToggle"
import { NavbarLogo, NavbarLogoProps } from "./Logo"
import { NavbarMobileMenu } from "./MobileMenu"
import { NavbarSearchModalOpener } from "./SearchModalOpener"
import { NavbarMobileMenuButtonProps } from "./MobileMenu/Button"
export type NavbarProps = {
logo: NavbarLogoProps
items: NavbarLinkProps[]
showSearchOpener?: boolean
showColorModeToggle?: boolean
additionalActions?: React.ReactNode
mobileMenuButton: NavbarMobileMenuButtonProps
isLoading?: boolean
className?: string
}
export const Navbar = ({
logo,
items,
showSearchOpener = true,
showColorModeToggle = true,
additionalActions,
mobileMenuButton,
isLoading,
className,
}: NavbarProps) => {
return (
<nav
className={clsx(
"h-navbar sticky top-0 w-full justify-between",
"bg-docs-bg dark:bg-docs-bg-dark border-medusa-border-base dark:border-medusa-border-base-dark z-[400] border-b",
className
)}
>
<div
className={clsx(
"h-navbar max-w-xxl py-docs_0.75 sticky top-0 mx-auto flex w-full justify-between px-docs_1 lg:px-docs_3"
)}
>
<div className="hidden w-full items-center gap-docs_0.5 lg:flex lg:w-auto lg:gap-docs_1.5">
<NavbarLogo {...logo} />
{items.map((item, index) => (
<NavbarLink key={index} {...item} />
))}
</div>
<div className="hidden min-w-0 flex-1 items-center justify-end gap-docs_0.5 lg:flex">
{showSearchOpener && (
<NavbarSearchModalOpener isLoading={isLoading} />
)}
{showColorModeToggle && <NavbarColorModeToggle />}
{additionalActions}
</div>
<NavbarMobileMenu
logo={logo}
menuButton={{
...mobileMenuButton,
isLoading,
}}
/>
</div>
</nav>
)
}
@@ -0,0 +1,27 @@
import React from "react"
import NextLink from "next/link"
import type { LinkProps as NextLinkProps } from "next/link"
import clsx from "clsx"
export type LinkProps = {
href?: string
children?: React.ReactNode
className?: string
} & Partial<NextLinkProps> &
React.AllHTMLAttributes<HTMLAnchorElement>
export const Link = ({ href, children, className, ...rest }: LinkProps) => {
return (
<NextLink
href={href || ""}
{...rest}
className={clsx(
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
"dark:text-medusa-fg-interactive-dark dark:hover:text-medusa-fg-interactive-hover-dark",
className
)}
>
{children}
</NextLink>
)
}
@@ -0,0 +1,89 @@
import React from "react"
import { NotificationItemProps } from "../.."
import clsx from "clsx"
import {
CheckCircleSolid,
ExclamationCircleSolid,
InformationCircleSolid,
XCircleSolid,
} from "@medusajs/icons"
import { Button } from "@/components"
export type NotificationItemLayoutDefaultProps = NotificationItemProps & {
handleClose: () => void
}
export const NotificationItemLayoutDefault: React.FC<
NotificationItemLayoutDefaultProps
> = ({
type = "info",
title = "",
text = "",
children,
isClosable = true,
handleClose,
CustomIcon,
}) => {
return (
<>
<div className={clsx("flex gap-docs_1 p-docs_1")}>
{type !== "none" && (
<div
className={clsx(
type !== "custom" && "w-docs_2 flex justify-center items-center"
)}
>
{type === "info" && (
<InformationCircleSolid className="text-medusa-fg-interactive-dark" />
)}
{type === "error" && (
<XCircleSolid className="text-medusa-tag-red-icon dark:text-medusa-tag-red-icon-dark" />
)}
{type === "warning" && (
<ExclamationCircleSolid className="text-medusa-tag-orange-icon dark:text-medusa-tag-orange-icon-dark" />
)}
{type === "success" && (
<CheckCircleSolid className="text-medusa-tag-green-icon dark:text-medusa-tag-green-icon-dark" />
)}
{type === "custom" && CustomIcon}
</div>
)}
<span
className={clsx(
"text-compact-medium-plus",
"text-medusa-fg-base dark:text-medusa-fg-base-dark"
)}
>
{title}
</span>
</div>
{(text || children) && (
<div
className={clsx(
"flex pt-0 pr-docs_1 pb-docs_1.5 pl-docs_1 gap-docs_1",
"border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark"
)}
>
<div className="w-docs_2 flex-none"></div>
<div className={clsx("flex flex-col", children && "gap-docs_1")}>
{text && (
<span
className={clsx(
"text-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark"
)}
>
{text}
</span>
)}
{children}
</div>
</div>
)}
{isClosable && (
<div className={clsx("p-docs_1 flex justify-end items-center")}>
<Button onClick={handleClose}>Close</Button>
</div>
)}
</>
)
}
@@ -0,0 +1,63 @@
import clsx from "clsx"
import React, { Children, ReactElement } from "react"
import { NotificationItemLayoutDefault } from "./Layout/Default"
export type NotificationItemProps = {
layout?: "default" | "empty"
type?: "info" | "error" | "warning" | "success" | "custom" | "none"
CustomIcon?: React.ReactNode
title?: string
text?: string
className?: string
children?: ReactElement
isClosable?: boolean
placement?: "top" | "bottom"
show?: boolean
setShow?: (value: boolean) => void
onClose?: () => void
} & React.HTMLAttributes<HTMLDivElement>
export const NotificationItem = ({
className = "",
placement = "bottom",
show = true,
layout = "default",
setShow,
onClose,
children,
...rest
}: NotificationItemProps) => {
const handleClose = () => {
setShow?.(false)
onClose?.()
}
return (
<div
className={clsx(
"md:max-w-[320px] md:w-[320px] w-full bg-medusa-bg-base dark:bg-medusa-bg-base-dark rounded-docs_DEFAULT",
"shadow-flyout dark:shadow-flyout-dark max-h-[calc(100vh-90px)]",
"fixed md:right-docs_1 left-0 z-[400] md:m-docs_1",
placement === "bottom" && "md:bottom-docs_1 bottom-0",
placement === "top" && "md:top-docs_1 top-0",
"opacity-100 transition-opacity duration-200 ease-ease",
!show && "!opacity-0",
className
)}
>
{layout === "default" && (
<NotificationItemLayoutDefault {...rest} handleClose={handleClose}>
{children}
</NotificationItemLayoutDefault>
)}
{layout === "empty" &&
Children.map(children, (child) => {
if (child) {
return React.cloneElement(child, {
onClose: handleClose,
})
}
})}
</div>
)
}
@@ -0,0 +1,58 @@
import { NotificationItemType, useNotifications } from "@/providers"
import React from "react"
import { NotificationItem } from "./Item"
import { CSSTransition, TransitionGroup } from "react-transition-group"
import clsx from "clsx"
export const NotificationContainer = () => {
const { notifications, removeNotification } = useNotifications()
const handleClose = (notification: NotificationItemType) => {
notification.onClose?.()
if (notification.id) {
removeNotification(notification.id)
}
}
const renderFilteredNotifications = (
condition: (notificaiton: NotificationItemType) => boolean,
className?: string
) => {
return (
<TransitionGroup className={className}>
{notifications.filter(condition).map((notification) => (
<CSSTransition
key={notification.id}
timeout={200}
classNames={{
enter: "animate__animated animate__slideInRight animate__fastest",
exit: "animate__animated animate__slideOutRight animate__fastest",
}}
>
<NotificationItem
{...notification}
onClose={() => handleClose(notification)}
className={clsx(
notification.className,
"!relative !top-0 !bottom-0 !right-0"
)}
/>
</CSSTransition>
))}
</TransitionGroup>
)
}
return (
<>
{renderFilteredNotifications(
(notification) => notification.placement === "top",
"flex fixed flex-col gap-docs_0.5 right-0 top-0 z-[400] md:w-auto w-full max-h-[calc(100vh-57px)] overflow-y-auto"
)}
{renderFilteredNotifications(
(notification) => notification.placement !== "top",
"flex flex-col gap-docs_0.5 fixed right-0 bottom-0 z-[400] md:w-auto w-full max-h-[calc(100vh-57px)] overflow-y-auto"
)}
</>
)
}
@@ -0,0 +1,80 @@
"use client"
import React, { useRef, useState } from "react"
import clsx from "clsx"
import { Star, StarSolid } from "@medusajs/icons"
import { Button } from "@/components"
import { useAnalytics } from "@/providers"
export type RatingProps = {
event?: string
className?: string
onRating?: () => void
} & React.HTMLAttributes<HTMLDivElement>
export const Rating: React.FC<RatingProps> = ({
event = "rating",
className = "",
onRating,
}) => {
const [rating, setRating] = useState(0)
const [hoverRating, setHoverRating] = useState(0)
const starElms = useRef<HTMLElement[]>([])
const starArr = Array.from(Array(5).keys())
const { track } = useAnalytics()
const handleRating = (selectedRating: number) => {
if (rating) {
return
}
setHoverRating(0)
setRating(selectedRating)
for (let i = 0; i < selectedRating; i++) {
starElms.current[i].classList.add("animate-tada")
}
track(
event,
{
rating: selectedRating,
},
() => onRating?.()
)
}
return (
<div className={clsx("flex gap-docs_0.5", className)}>
{starArr.map((i) => {
const isSelected =
(rating !== 0 && rating - 1 >= i) ||
(hoverRating !== 0 && hoverRating - 1 >= i)
return (
<Button
variant="clear"
buttonRef={(element) => {
if (starElms.current.length - 1 < i) {
starElms.current.push(element as HTMLElement)
}
}}
key={i}
onMouseOver={() => {
if (!rating) {
setHoverRating(i + 1)
}
}}
onMouseLeave={() => {
if (!rating) {
setHoverRating(0)
}
}}
onClick={() => handleRating(i + 1)}
>
{!isSelected && <Star />}
{isSelected && (
<StarSolid className="text-medusa-tag-orange-icon dark:text-medusa-tag-orange-icon-dark" />
)}
</Button>
)
})}
</div>
)
}
@@ -0,0 +1,22 @@
"use client"
import React from "react"
import { useInstantSearch } from "react-instantsearch"
export type SearchEmptyQueryBoundaryProps = {
children: React.ReactNode
fallback: React.ReactNode
}
export const SearchEmptyQueryBoundary = ({
children,
fallback,
}: SearchEmptyQueryBoundaryProps) => {
const { indexUiState } = useInstantSearch()
if (!indexUiState.query) {
return <>{fallback}</>
}
return <>{children}</>
}
@@ -0,0 +1,20 @@
import React from "react"
import clsx from "clsx"
export type SearchHitGroupNameProps = {
name: string
}
export const SearchHitGroupName = ({ name }: SearchHitGroupNameProps) => {
return (
<span
className={clsx(
"pb-docs_0.25 flex px-docs_0.5 pt-docs_1",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark",
"text-compact-x-small-plus"
)}
>
{name}
</span>
)
}
@@ -0,0 +1,215 @@
"use client"
import React, { Fragment, useEffect, useMemo, useState } from "react"
import clsx from "clsx"
import {
Configure,
ConfigureProps,
Index,
Snippet,
useHits,
useInstantSearch,
} from "react-instantsearch"
import { SearchNoResult } from "../NoResults"
import { SearchHitGroupName } from "./GroupName"
import { useSearch } from "@/providers"
import { Link } from "@/components/Link"
export 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
}
export type GroupedHitType = {
[k: string]: HitType[]
}
export type SearchHitWrapperProps = {
configureProps: ConfigureProps
indices: string[]
} & Omit<SearchHitsProps, "indexName" | "setNoResults">
export type IndexResults = {
[k: string]: boolean
}
export const SearchHitsWrapper = ({
configureProps,
indices,
...rest
}: SearchHitWrapperProps) => {
const { status } = useInstantSearch()
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}
{...rest}
/>
<Configure {...configureProps} />
</Index>
))}
</div>
)
}
export type SearchHitsProps = {
indexName: string
setNoResults: (index: string, value: boolean) => void
checkInternalPattern?: RegExp
}
export const SearchHits = ({
indexName,
setNoResults,
checkInternalPattern,
}: 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 checkIfInternal = (url: string): boolean => {
if (!checkInternalPattern) {
return false
}
return checkInternalPattern.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-docs_0.25 relative flex flex-1 flex-col p-docs_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",
"last:mb-docs_1 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>
)
}
@@ -0,0 +1,334 @@
"use client"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { InstantSearch, SearchBox } from "react-instantsearch"
import clsx from "clsx"
import { SearchEmptyQueryBoundary } from "../EmptyQueryBoundary"
import { SearchSuggestions, type SearchSuggestionType } from "../Suggestions"
import { AlgoliaProps, useSearch } from "@/providers"
import { checkArraySameElms } from "@/utils"
import { SearchHitsWrapper } from "../Hits"
import { findNextSibling, findPrevSibling } from "@/utils"
import { Button, Kbd, Modal, SelectBadge } from "@/components"
import { MagnifyingGlass, XMark } from "@medusajs/icons"
import { useKeyboardShortcut, type OptionType } from "@/hooks"
export type SearchModalProps = {
algolia: AlgoliaProps
isLoading?: boolean
suggestions: SearchSuggestionType[]
checkInternalPattern?: RegExp
filterOptions?: OptionType[]
}
export const SearchModal = ({
algolia,
suggestions,
isLoading = false,
checkInternalPattern,
filterOptions = [],
}: SearchModalProps) => {
const modalRef = useRef<HTMLDialogElement | null>(null)
const { isOpen, setIsOpen, defaultFilters, searchClient } = 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()
} else if (!isOpen) {
const focusedItem = modalRef.current?.querySelector(
":focus"
) as HTMLElement
if (
focusedItem &&
focusedItem === searchBoxRef.current?.querySelector("input")
) {
// remove focus
focusedItem.blur()
}
}
}, [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()
}
}
}
}
const shortcutKeys = useMemo(() => ["ArrowUp", "ArrowDown", "Enter"], [])
useKeyboardShortcut({
metakey: false,
shortcutKeys,
action: handleKeyAction,
checkEditing: false,
preventDefault: false,
isLoading,
})
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!isOpen) {
return
}
// check if shortcut keys were pressed
const lowerPressedKey = e.key.toLowerCase()
const pressedShortcut = [...shortcutKeys, "Escape"].some(
(s) => s.toLowerCase() === lowerPressedKey
)
if (pressedShortcut) {
return
}
const focusedItem = modalRef.current?.querySelector(
":focus"
) as HTMLElement
const searchInput = searchBoxRef.current?.querySelector(
"input"
) as HTMLInputElement
if (searchInput && focusedItem !== searchInput) {
searchInput.focus()
}
},
[shortcutKeys, isOpen]
)
useEffect(() => {
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}, [handleKeyDown])
return (
<Modal
contentClassName={clsx(
"!p-0 overflow-hidden relative h-full",
"rounded-none md:rounded-docs_lg flex flex-col justify-between"
)}
modalContainerClassName="w-screen h-screen !rounded-none md:!rounded-docs_lg"
open={isOpen}
onClose={() => setIsOpen(false)}
passedRef={modalRef}
>
<InstantSearch
indexName={algolia.mainIndexName}
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-docs_xl relative border-0 border-solid",
"border-b border-medusa-border-base dark:border-medusa-border-base-dark",
"bg-transparent"
),
form: clsx("h-full md:rounded-t-docs_xl bg-transparent"),
input: clsx(
"w-full h-full pl-docs_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-docs_xl text-compact-medium bg-transparent",
"appearance-none search-cancel:hidden border-0 active:outline-none focus:outline-none"
),
submit: clsx("absolute top-[18px] left-docs_1 btn-clear p-0"),
reset: clsx(
"absolute top-docs_0.75 right-docs_1 hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"p-[5px] md:rounded-docs_DEFAULT btn-clear"
),
loadingIndicator: clsx("absolute top-[18px] right-docs_1"),
}}
submitIconComponent={() => (
<MagnifyingGlass className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
resetIconComponent={() => (
<XMark className="hidden md:block text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark" />
)}
placeholder="Find something..."
autoFocus
formRef={searchBoxRef}
/>
<Button
variant="clear"
className={clsx(
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark block md:hidden",
"border-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark border-b",
"pr-docs_1"
)}
onClick={() => setIsOpen(false)}
>
<XMark className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
</Button>
</div>
<div className="mx-docs_0.5 h-[calc(100%-120px)] md:h-[332px] md:flex-initial lg:max-h-[332px] lg:min-h-[332px]">
<SearchEmptyQueryBoundary
fallback={<SearchSuggestions suggestions={suggestions} />}
>
<SearchHitsWrapper
configureProps={{
filters: formattedFilters,
attributesToSnippet: [
"content",
"hierarchy.lvl1",
"hierarchy.lvl2",
],
attributesToHighlight: [
"content",
"hierarchy.lvl1",
"hierarchy.lvl2",
],
}}
indices={algolia.indices}
checkInternalPattern={checkInternalPattern}
/>
</SearchEmptyQueryBoundary>
</div>
</InstantSearch>
<div
className={clsx(
"py-docs_0.75 flex items-center justify-between px-docs_1",
"border-0 border-solid",
"border-medusa-border-base dark:border-medusa-border-base-dark border-t",
"bg-medusa-bg-base dark:bg-medusa-bg-base-dark"
)}
>
{filterOptions.length && (
<SelectBadge
multiple
options={filterOptions}
value={filters}
setSelected={(value) =>
setFilters(Array.isArray(value) ? [...value] : [value])
}
addSelected={(value) => setFilters((prev) => [...prev, value])}
removeSelected={(value) =>
setFilters((prev) => prev.filter((v) => v !== value))
}
showClearButton={false}
placeholder="Filters"
handleAddAll={(isAllSelected: boolean) => {
if (isAllSelected) {
setFilters(defaultFilters)
} else {
setFilters(filterOptions.map((option) => option.value))
}
}}
/>
)}
<div className="hidden items-center gap-docs_1 md:flex">
<div className="flex items-center gap-docs_0.5">
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
Navigation
</span>
<span className="gap-docs_0.25 flex">
<Kbd></Kbd>
<Kbd></Kbd>
</span>
</div>
<div className="flex items-center gap-docs_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>
)
}
@@ -0,0 +1,90 @@
"use client"
import React, { MouseEvent, useMemo } from "react"
import clsx from "clsx"
import { useSearch } from "@/providers"
import { Button, InputText, Kbd } from "@/components"
import { MagnifyingGlass } from "@medusajs/icons"
import { useKeyboardShortcut } from "@/hooks"
export type SearchModalOpenerProps = {
isLoading?: boolean
isMobile?: boolean
}
export const SearchModalOpener = ({
isLoading = false,
isMobile = false,
}: SearchModalOpenerProps) => {
const { setIsOpen } = useSearch()
const isApple = useMemo(() => {
return typeof navigator !== "undefined"
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
: true
}, [])
useKeyboardShortcut({
shortcutKeys: ["k"],
action: () => setIsOpen((prev) => !prev),
isLoading,
})
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}>
<MagnifyingGlass className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
</Button>
)}
{!isMobile && (
<div
className={clsx("relative w-min hover:cursor-pointer")}
onClick={handleOpen}
>
<MagnifyingGlass
className={clsx(
"absolute left-docs_0.5 top-[5px]",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark"
)}
/>
<InputText
type="search"
className={clsx(
"placeholder:text-compact-small",
"!py-[5px] !pl-[36px] !pr-docs_0.5",
"cursor-pointer select-none"
)}
placeholder="Find something"
onClick={handleOpen}
onFocus={(e) => e.target.blur()}
tabIndex={-1}
/>
<span
className={clsx(
"gap-docs_0.25 flex",
"absolute right-docs_0.5 top-[5px]"
)}
>
<Kbd>{isApple ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd>
</span>
</div>
)}
</>
)
}
@@ -0,0 +1,19 @@
import { ExclamationCircleSolid } from "@medusajs/icons"
import clsx from "clsx"
import React from "react"
export const SearchNoResult = () => {
return (
<div
className={clsx(
"flex h-full w-full flex-col items-center justify-center gap-docs_1",
"text-medusa-fg-muted dark:text-medusa-fg-muted-dark"
)}
>
<ExclamationCircleSolid />
<span className="text-compact-small">
No results found. Try changing selected filters.
</span>
</div>
)
}
@@ -0,0 +1,56 @@
"use client"
import React from "react"
import clsx from "clsx"
import { useInstantSearch } from "react-instantsearch"
import { SearchHitGroupName } from "../Hits/GroupName"
export type SearchSuggestionType = {
title: string
items: string[]
}
export type SearchSuggestionsProps = {
suggestions: SearchSuggestionType[]
}
export const SearchSuggestions = ({ suggestions }: SearchSuggestionsProps) => {
const { setIndexUiState } = useInstantSearch()
return (
<div className="h-full overflow-auto">
{suggestions.map((suggestion, index) => (
<React.Fragment key={index}>
<SearchHitGroupName name={suggestion.title} />
{suggestion.items.map((item, itemIndex) => (
<div
className={clsx(
"flex items-center justify-between",
"cursor-pointer rounded-docs_sm p-docs_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 last:mb-docs_1"
)}
onClick={() =>
setIndexUiState({
query: item,
})
}
key={itemIndex}
tabIndex={itemIndex}
data-hit
>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-small"
)}
>
{item}
</span>
</div>
))}
</React.Fragment>
))}
</div>
)
}
@@ -0,0 +1,6 @@
export * from "./EmptyQueryBoundary"
export * from "./Hits"
export * from "./Modal"
export * from "./ModalOpener"
export * from "./NoResults"
export * from "./Suggestions"
@@ -0,0 +1,136 @@
"use client"
import React, { useCallback, useRef, useState } from "react"
import { useSelect } from "@/hooks"
import clsx from "clsx"
import { SelectDropdown, SelectProps } from ".."
export const SelectBadge = ({
value,
options,
setSelected,
addSelected,
removeSelected,
multiple,
className,
addAll = multiple,
handleAddAll,
...props
}: SelectProps) => {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const { isValueSelected, isAllSelected, handleChange, handleSelectAll } =
useSelect({
value,
options,
multiple,
setSelected,
removeSelected,
addSelected,
handleAddAll,
})
const getSelectedText = useCallback(() => {
let str = ""
const selectedOptions = options.filter((option) =>
value.includes(option.value)
)
if (isAllSelected) {
str = "All Areas"
} else {
if (
(!Array.isArray(value) && !value) ||
(Array.isArray(value) && !value.length)
) {
str = "None selected"
} else {
str = selectedOptions[0].label
}
}
return (
<>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark",
"text-compact-x-small-plus",
"inline-block max-w-[60px] overflow-hidden text-ellipsis"
)}
>
{str}
</span>
{!isAllSelected && selectedOptions.length > 1 && (
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small"
)}
>
{" "}
+ {selectedOptions.length}
</span>
)}
</>
)
}, [isAllSelected, options, value])
return (
<div className={clsx("relative", className)}>
<div
className={clsx(
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-sm border border-solid",
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-subtle-hover-dark",
"py-docs_0.25 h-fit cursor-pointer px-docs_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}
passedRef={dropdownRef}
className={clsx(
"!top-[unset] !bottom-full",
open && "!-translate-y-docs_0.5"
)}
/>
</div>
)
}
export default SelectBadge
@@ -0,0 +1,148 @@
"use client"
import React, { useCallback, useEffect, useRef } from "react"
import clsx from "clsx"
import { OptionType } from "@/hooks"
import { Ref } from "@/types"
import { CheckMini, EllipseMiniSolid } from "@medusajs/icons"
export 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
passedRef?: Ref<HTMLDivElement>
}
export const SelectDropdown = ({
open,
setOpen,
options,
addAll,
multiple = false,
isAllSelected,
isValueSelected,
handleSelectAll,
handleChange: handleSelectChange,
parentRef,
className,
passedRef,
}: SelectDropdownProps) => {
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-docs_0.75 relative rounded-docs_sm py-docs_0.5 pl-docs_2.5",
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark",
"[&>svg]:left-docs_0.75 cursor-pointer [&>svg]:absolute [&>svg]:top-docs_0.5",
!isSelected && "text-compact-small",
isSelected && "text-compact-small-plus"
)}
onClick={() => {
if (option.isAllOption) {
handleSelectAll()
} else {
handleChange(option.value, isSelected)
}
}}
>
{isSelected && (
<>
{multiple && (
<CheckMini className="text-medusa-fg-base dark:text-medusa-fg-base-dark" />
)}
{!multiple && (
<EllipseMiniSolid className="text-medusa-fg-base dark:text-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-docs_0.5 !overflow-visible",
className
)}
ref={setRefs}
>
<ul
className={clsx(
"p-docs_0.25 mb-0 w-full overflow-auto rounded-docs_DEFAULT",
"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>
)
}
@@ -0,0 +1,125 @@
"use client"
import React, { useRef, useState } from "react"
import clsx from "clsx"
import { useSelect } from "@/hooks"
import { SelectDropdown, SelectProps } from ".."
import { Badge } from "docs-ui"
import { ChevronUpDown, XMarkMini } from "@medusajs/icons"
export 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-docs_0.75 relative py-[9px]",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-docs_sm border border-solid",
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-neutral dark:shadow-button-neutral-dark",
"hover:bg-medusa-bg-field-hover dark:hover:bg-medusa-bg-field-hover-dark",
"active:shadow-active dark:active:shadow-active-dark",
"focus:shadow-active dark:focus:shadow-active-dark",
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:text-medusa-fg-disabled dark:disabled:text-medusa-fg-disabled-dark",
"flex items-center gap-docs_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-docs_0.125"
)}
>
{(value as string[]).length}
</span>
{showClearButton && (
<XMarkMini
className="text-medusa-tag-neutral-icon dark:text-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>
<ChevronUpDown className="text-medusa-fg-muted dark:text-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}
passedRef={dropdownRef}
/>
</div>
)
}
@@ -0,0 +1,13 @@
import type { OptionType, SelectOptions } from "@/hooks"
export type SelectProps = {
options: OptionType[]
multiple?: boolean
addAll?: boolean
showClearButton?: boolean
} & SelectOptions &
React.ComponentProps<"input">
export * from "./Badge"
export * from "./Dropdown"
export * from "./Input"
@@ -0,0 +1,126 @@
"use client"
import React, { useEffect, useMemo, useRef, useState } from "react"
import type { SidebarItemType } from "@/providers"
import { useSidebar } from "@/providers"
import clsx from "clsx"
import Link from "next/link"
import { checkSidebarItemVisibility } from "@/utils"
import { Loading } from "@/components"
export type SidebarItemProps = {
item: SidebarItemType
nested?: boolean
expandItems?: boolean
} & React.AllHTMLAttributes<HTMLLIElement>
export const SidebarItem = ({
item,
nested = false,
expandItems = false,
className,
}: SidebarItemProps) => {
const [showLoading, setShowLoading] = useState(false)
const { isItemActive, setMobileSidebarOpen: setSidebarOpen } = useSidebar()
const active = useMemo(() => {
return isItemActive(item, nested)
}, [isItemActive, item, nested])
const collapsed = !expandItems && !isItemActive(item, true)
const ref = useRef<HTMLLIElement>(null)
useEffect(() => {
if (active && ref.current && window.innerWidth >= 1025) {
if (
!checkSidebarItemVisibility(ref.current, {
topMargin: 57,
})
) {
// scroll to element
ref.current.scrollIntoView({
block: "center",
})
}
}
if (active) {
setShowLoading(true)
}
}, [active])
const classNames = useMemo(
() =>
clsx(
"flex items-center justify-between gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
!item.children &&
"text-compact-small-plus text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
item.children &&
"text-compact-x-small-plus text-medusa-fg-muted dark:text-medusa-fg-muted-dark uppercase",
item.path !== undefined &&
active && [
"!text-medusa-fg-base bg-medusa-bg-base-pressed",
"dark:!text-medusa-fg-base-dark dark:bg-medusa-bg-base-pressed-dark",
],
item.path !== undefined &&
active &&
"border border-medusa-border-base dark:border-medusa-border-base-dark",
item.path !== undefined &&
!active &&
"hover:bg-medusa-bg-base-hover dark:hover:bg-medusa-bg-base-hover-dark border-transparent"
),
[item.children, active, item.path]
)
return (
<li
className={clsx(
item.children && !collapsed && "my-docs_1.5",
!item.children && !nested && active && "mt-docs_1.5",
!expandItems &&
((item.children && !collapsed) ||
(!item.children && !nested && active)) &&
"-translate-y-docs_1 transition-transform",
className
)}
ref={ref}
>
{item.path === undefined && (
<span className={classNames}>
<span>{item.title}</span>
{item.additionalElms}
</span>
)}
{item.path !== undefined && (
<Link
href={item.isPathHref ? item.path : `#${item.path}`}
className={classNames}
scroll={true}
onClick={() => {
if (window.innerWidth < 1025) {
setSidebarOpen(false)
}
}}
replace
shallow
>
<span>{item.title}</span>
{item.additionalElms}
</Link>
)}
{item.children && (
<ul
className={clsx("ease-ease overflow-hidden", collapsed && "m-0 h-0")}
>
{showLoading && !item.loaded && (
<Loading
count={3}
className="!mb-0 !px-docs_0.5"
barClassName="h-[20px]"
/>
)}
{item.children?.map((childItem, index) => (
<SidebarItem item={childItem} key={index} nested={true} />
))}
</ul>
)}
</li>
)
}
@@ -0,0 +1,63 @@
"use client"
import React from "react"
import { useSidebar } from "@/providers"
import clsx from "clsx"
import { Loading } from "@/components"
import { SidebarItem } from "./Item"
export type SidebarProps = {
className?: string
expandItems?: boolean
}
export const Sidebar = ({
className = "",
expandItems = false,
}: SidebarProps) => {
const { items, mobileSidebarOpen, desktopSidebarOpen } = useSidebar()
return (
<aside
className={clsx(
"clip bg-docs-bg dark:bg-docs-bg-dark w-ref-sidebar block",
"border-medusa-border-base dark:border-medusa-border-base-dark border-0 border-r border-solid",
"fixed -left-full top-[57px] h-screen transition-[left] lg:relative lg:left-0 lg:top-auto lg:h-auto",
"lg:w-sidebar z-[100] w-full lg:z-0",
mobileSidebarOpen && "!left-0",
!desktopSidebarOpen && "!absolute !-left-full",
className
)}
style={{
animationFillMode: "forwards",
}}
>
<ul
className={clsx(
"sticky top-[57px] h-screen max-h-screen w-full list-none overflow-auto p-0",
"px-docs_1.5 pb-[57px] pt-docs_1.5"
)}
id="sidebar"
>
<div className="mb-docs_1.5 lg:hidden">
{!items.mobile.length && <Loading className="px-0" />}
{items.mobile.map((item, index) => (
<SidebarItem item={item} key={index} expandItems={expandItems} />
))}
</div>
<div className="mb-docs_1.5">
{!items.top.length && <Loading className="px-0" />}
{items.top.map((item, index) => (
<SidebarItem item={item} key={index} expandItems={expandItems} />
))}
</div>
<div className="mb-docs_1.5">
{!items.bottom.length && <Loading className="px-0" />}
{items.bottom.map((item, index) => (
<SidebarItem item={item} key={index} expandItems={expandItems} />
))}
</div>
</ul>
</aside>
)
}
@@ -0,0 +1,30 @@
import React from "react"
import clsx from "clsx"
export type TextAreaProps = {
className?: string
} & React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>
export const TextArea = (props: TextAreaProps) => {
return (
<textarea
{...props}
className={clsx(
"bg-medusa-bg-field dark:bg-medusa-bg-field-dark shadow-button-secondary dark:shadow-button-secondary-dark",
"border-medusa-border-base dark:border-medusa-border-base-dark rounded-docs_sm border border-solid",
"pt-docs_0.4 px-docs_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",
"active:border-medusa-border-interactive dark:active:border-medusa-border-interactive-dark",
"disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark",
"disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark",
"placeholder:text-medusa-fg-muted dark:placeholder:text-medusa-fg-muted-dark",
"disabled:placeholder:text-medusa-fg-disabled dark:disabled:placeholder:text-medusa-fg-disabled-dark",
props.className
)}
/>
)
}
@@ -0,0 +1,64 @@
"use client"
import React, { useEffect, useState } from "react"
import { Tooltip as ReactTooltip } from "react-tooltip"
import type { ITooltip } from "react-tooltip"
import clsx from "clsx"
import "react-tooltip/dist/react-tooltip.css"
import uuid from "react-uuid"
export type TooltipProps = {
text?: string
tooltipClassName?: string
html?: string
tooltipChildren?: React.ReactNode
} & React.HTMLAttributes<HTMLSpanElement> &
ITooltip
export const Tooltip = ({
text = "",
tooltipClassName = "",
children,
html = "",
tooltipChildren,
...tooltipProps
}: TooltipProps) => {
const [elementId, setElementId] = useState<string>("")
useEffect(() => {
if (!elementId) {
setElementId(uuid())
}
}, [elementId])
return (
<>
<span
id={elementId}
data-tooltip-content={text}
data-tooltip-html={html}
data-tooltip-id={elementId}
>
{children}
</span>
<ReactTooltip
anchorId={elementId}
// anchorSelect={elementId ? `#${elementId}` : undefined}
className={clsx(
"!border-medusa-border-base dark:!border-medusa-border-base-dark !border !border-solid",
"!text-compact-x-small-plus !shadow-tooltip dark:!shadow-tooltip-dark !rounded-docs_DEFAULT",
"!py-docs_0.4 !z-[399] hidden !px-docs_1 lg:block",
"!bg-medusa-bg-base dark:!bg-medusa-bg-base-dark",
"!text-medusa-fg-subtle dark:!text-medusa-fg-subtle-dark",
tooltipClassName
)}
wrapper="span"
noArrow={true}
positionStrategy={"fixed"}
{...tooltipProps}
>
{tooltipChildren}
</ReactTooltip>
</>
)
}
@@ -0,0 +1,42 @@
export * from "./Badge"
export * from "./Bordered"
export * from "./BorderedIcon"
export * from "./Button"
export * from "./Card"
export * from "./CodeBlock"
export * from "./CodeMdx"
export * from "./CodeTabs"
export * from "./CopyButton"
export * from "./Details"
export * from "./Details/Summary"
export * from "./Feedback"
export * from "./Feedback/Solutions"
export * from "./InlineCode"
export * from "./Input/Text"
export * from "./Kbd"
export * from "./Label"
export * from "./Link"
export * from "./Loading"
export * from "./Loading/Spinner"
export * from "./Modal"
export * from "./Modal/Header"
export * from "./Modal/Footer"
export * from "./Navbar"
export * from "./Navbar/ColorModeToggle"
export * from "./Navbar/IconButton"
export * from "./Navbar/Link"
export * from "./Navbar/Logo"
export * from "./Navbar/MobileMenu"
export * from "./Navbar/MobileMenu/Button"
export * from "./Navbar/SearchModalOpener"
export { Link as NextLink, type LinkProps as NextLinkProps } from "./NextLink"
export * from "./Notification"
export * from "./Notification/Item"
export * from "./Notification/Item/Layout/Default"
export * from "./Rating"
export * from "./Search"
export * from "./Select"
export * from "./Sidebar"
export * from "./Sidebar/Item"
export * from "./TextArea"
export * from "./Tooltip"