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

View File

@@ -0,0 +1,33 @@
import React from "react"
import type { BorderedIconProps as UiBorderedIconProps } from "docs-ui"
import { BorderedIcon as UiBorderedIcon } from "docs-ui"
import { useColorMode } from "@docusaurus/theme-common"
type BorderedIconProps = {
icon?: {
light: string
dark?: string
}
} & Omit<UiBorderedIconProps, "icon">
const BorderedIcon: React.FC<BorderedIconProps> = ({
icon = null,
...props
}) => {
const { colorMode } = useColorMode()
return (
<UiBorderedIcon
{...props}
icon={
icon
? colorMode === "light"
? icon.light
: icon.dark || icon.light
: ""
}
/>
)
}
export default BorderedIcon

View File

@@ -0,0 +1,106 @@
import React from "react"
import { useThemeConfig } from "@docusaurus/theme-common"
// @ts-expect-error: wait until docusaurus uses type: module
import { Cloudinary } from "@cloudinary/url-gen"
import MDXImg, { Props as MDXImgProps } from "@theme/MDXComponents/Img"
import {
pad,
imaggaScale,
imaggaCrop,
crop,
fit,
minimumPad,
fill,
scale,
limitFit,
thumbnail,
limitFill,
minimumFit,
limitPad,
fillPad,
} from "@cloudinary/url-gen/actions/resize"
import { byRadius } from "@cloudinary/url-gen/actions/roundCorners"
import { ThemeConfig } from "@medusajs/docs"
const resizeActions = {
pad: pad,
imaggaScale: imaggaScale,
imaggaCrop: imaggaCrop,
crop: crop,
fit: fit,
minimumPad: minimumPad,
fill: fill,
scale: scale,
limitFit: limitFit,
thumbnail: thumbnail,
limitFill: limitFill,
minimumFit: minimumFit,
limitPad: limitPad,
fillPad: fillPad,
}
const imageRegex =
/^https:\/\/res.cloudinary.com\/.*\/upload\/v[0-9]+\/(?<imageId>.*)$/
type CloudinaryImageProps = MDXImgProps
const CloudinaryImage: React.FC<CloudinaryImageProps> = ({ src, ...props }) => {
const { cloudinaryConfig } = useThemeConfig() as ThemeConfig
const matchingRegex = src.match(imageRegex)
if (
!cloudinaryConfig ||
!matchingRegex?.groups ||
!matchingRegex.groups.imageId
) {
// either cloudinary isn't configured or
// could not match url to a cloudinary url
// default to docusaurus's image component
return <MDXImg src={src} {...props} />
}
const cloudinary = new Cloudinary({
cloud: {
cloudName: cloudinaryConfig.cloudName,
},
})
const image = cloudinary.image(
matchingRegex.groups.imageId.replaceAll("%20", " ")
)
cloudinaryConfig.flags?.forEach((flag) => image.addTransformation(flag))
if (cloudinaryConfig.roundCorners) {
image.roundCorners(byRadius(cloudinaryConfig.roundCorners))
}
if (cloudinaryConfig.resize) {
const action = resizeActions[cloudinaryConfig.resize.action]
let resizeAction = action()
if (props.width || props.height) {
if (props.width) {
resizeAction = resizeAction.width(props.width)
}
if (props.height) {
resizeAction = resizeAction.height(props.height)
}
} else if (cloudinaryConfig.resize.aspectRatio) {
resizeAction = resizeAction.aspectRatio(
cloudinaryConfig.resize.aspectRatio
)
} else {
if (cloudinaryConfig.resize.width) {
resizeAction = resizeAction.width(cloudinaryConfig.resize.width)
}
if (cloudinaryConfig.resize.height) {
resizeAction = resizeAction.height(cloudinaryConfig.resize.height)
}
}
image.resize(resizeAction)
}
return <MDXImg {...props} src={image.toURL()} />
}
export default CloudinaryImage

View File

@@ -0,0 +1,47 @@
import React from "react"
import icons from "../../theme/Icon"
import BorderedIcon from "../BorderedIcon"
import clsx from "clsx"
type DocSidebarItemIconProps = {
icon?: string
is_title?: boolean
is_disabled?: boolean
} & React.HTMLAttributes<HTMLSpanElement>
const DocSidebarItemIcon: React.FC<DocSidebarItemIconProps> = ({
icon,
is_title,
is_disabled,
}) => {
const IconComponent = icons[icon]
return (
<>
{is_title && (
<BorderedIcon
icon={null}
IconComponent={IconComponent}
iconClassName={clsx("sidebar-item-icon")}
iconColorClassName={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
is_disabled &&
"text-medusa-fg-disabled dark:text-medusa-fg-disabled-dark"
)}
/>
)}
{!is_title && (
<IconComponent
className={clsx(
"sidebar-item-icon",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
is_disabled &&
"text-medusa-fg-disabled dark:text-medusa-fg-disabled-dark"
)}
/>
)}
</>
)
}
export default DocSidebarItemIcon

View File

@@ -0,0 +1,28 @@
import React from "react"
import {
Feedback as UiFeedback,
type FeedbackProps as UiFeedbackProps,
GITHUB_ISSUES_PREFIX,
} from "docs-ui"
import useIsBrowser from "@docusaurus/useIsBrowser"
import { useLocation } from "@docusaurus/router"
import clsx from "clsx"
type FeedbackProps = Omit<UiFeedbackProps, "pathName" | "reportLink">
const Feedback = (props: FeedbackProps) => {
const isBrowser = useIsBrowser()
const location = useLocation()
return (
<UiFeedback
{...props}
className={clsx("py-2", props.className)}
pathName={isBrowser && location ? location.pathname : ""}
reportLink={GITHUB_ISSUES_PREFIX}
showLongForm={true}
/>
)
}
export default Feedback

View File

@@ -0,0 +1,39 @@
import React from "react"
import IconTwitter from "@site/src/theme/Icon/Twitter"
import IconGitHub from "@site/src/theme/Icon/GitHub"
import IconDiscord from "@site/src/theme/Icon/Discord"
import IconLinkedIn from "@site/src/theme/Icon/LinkedIn"
import { SocialLink } from "@medusajs/docs"
type SocialLinksProps = {
links?: SocialLink[]
} & React.HTMLAttributes<HTMLDivElement>
const SocialLinks: React.FC<SocialLinksProps> = ({ links = [] }) => {
const socialIcons = {
twitter: (
<IconTwitter className="text-ui-fg-muted group-hover:text-ui-fg-subtle" />
),
github: (
<IconGitHub className="text-ui-fg-muted group-hover:text-ui-fg-subtle" />
),
discord: (
<IconDiscord className="text-ui-fg-muted group-hover:text-ui-fg-subtle" />
),
linkedin: (
<IconLinkedIn className="text-ui-fg-muted group-hover:text-ui-fg-subtle" />
),
}
return (
<div className="flex items-center">
{links.map((link) => (
<a className="group ml-1 first:ml-0" href={link.href} key={link.type}>
{socialIcons[link.type]}
</a>
))}
</div>
)
}
export default SocialLinks

View File

@@ -0,0 +1,50 @@
import React, { useMemo } from "react"
import Heading from "@theme/Heading"
import { GlossaryType, getGlossary } from "../../utils/glossary"
import Link from "@docusaurus/Link"
type GlossaryProps = React.HTMLAttributes<HTMLDivElement>
type GroupedGlossary = {
[letter: string]: GlossaryType[]
}
const Glossary: React.FC<GlossaryProps> = (props) => {
const groupedGlossary: GroupedGlossary = useMemo(() => {
const glossary = getGlossary()
glossary.sort((a, b) => {
return a.title.localeCompare(b.title)
})
const grouped: GroupedGlossary = {}
glossary.forEach((glossaryItem) => {
const firstChar = glossaryItem.title.charAt(0).toLowerCase()
grouped[firstChar] = [...(grouped[firstChar] || []), glossaryItem]
})
return grouped
}, [])
return (
<div {...props}>
{Object.entries(groupedGlossary).map(([letter, glossary], index) => (
<React.Fragment key={index}>
<Heading id={letter.toLowerCase()} as="h2">
{letter.toUpperCase()}
</Heading>
<ul>
{glossary.map((glossaryItem, index) => (
<li key={index}>
<Link to={glossaryItem.referenceLink}>
{glossaryItem.title}
</Link>
: {glossaryItem.content}
</li>
))}
</ul>
</React.Fragment>
))}
</div>
)
}
export default Glossary

View File

@@ -0,0 +1,101 @@
import React from "react"
import clsx from "clsx"
import Link from "@docusaurus/Link"
import { Badge } from "docs-ui"
import BorderedIcon from "../BorderedIcon"
type LargeCardProps = {
// TODO change to React.FC<IconProps>
// once react versions are resolved
Icon: any
image: {
light: string
dark?: string
}
title: string
action?: {
href?: string
}
isSoon?: boolean
className?: string
} & React.HTMLAttributes<HTMLDivElement>
const LargeCard: React.FC<LargeCardProps> = ({
Icon,
image,
title,
action: { href } = {
href: "",
},
isSoon = false,
className = "",
children,
}) => {
return (
<article
className={clsx(
"group bg-docs-bg-surface dark:bg-docs-bg-surface-dark",
"rounded",
"p-1 !pb-1.5 shadow-card-rest dark:shadow-card-rest-dark",
"flex flex-col justify-between relative",
"[&:nth-child(3n+1):before]:bg-[2%_52%] [&:nth-child(3n+2):before]:bg-[19%_16%] [&:nth-child(3n+3):before]:bg-[17%_50%]",
!isSoon &&
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-base-hover-dark hover:shadow-card-hover dark:hover:shadow-card-hover-dark",
!isSoon &&
"group-hover:bg-medusa-bg-subtle-hover dark:group-hover:bg-medusa-bg-base-hover-dark group-hover:shadow-card-hover dark:group-hover:shadow-card-hover-dark",
"transition-all duration-200 ease-ease",
"large-card",
className
)}
>
<div className={clsx("z-[3]")}>
{isSoon && (
<Badge variant={"purple"} className="absolute top-1 right-1">
Guide coming soon
</Badge>
)}
{(Icon || image) && (
<BorderedIcon
IconComponent={Icon}
icon={image}
iconClassName="w-[20px] h-[20px]"
wrapperClassName="mb-1"
iconWrapperClassName="p-[6px]"
/>
)}
<div className="mb-0.25">
<span
className={clsx(
isSoon &&
"group-hover:text-medusa-fg-disabled dark:group-hover:text-medusa-fg-disabled-dark",
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium-plus",
"transition-all duration-200 ease-ease"
)}
>
{title}
</span>
</div>
<div
className={clsx(
isSoon &&
"group-hover:text-medusa-fg-disabled dark:group-hover:text-medusa-fg-disabled-dark",
"transition-all duration-200 ease-ease text-medium"
)}
>
{children}
</div>
</div>
{href && (
<Link
href={href}
className={clsx(
"absolute top-0 left-0 w-full h-full z-[4] rounded",
isSoon && "group-hover:pointer-events-none"
)}
></Link>
)}
</article>
)
}
export default LargeCard

View File

@@ -0,0 +1,30 @@
import React from "react"
import clsx from "clsx"
type LargeCardListProps = {
colSize?: string
className?: string
} & React.HTMLAttributes<HTMLDivElement>
const LargeCardList: React.FC<LargeCardListProps> = ({
colSize = "4",
className,
children,
}) => {
return (
<section
className={clsx(
"cards-grid",
`grid-${colSize}`,
"gap-1",
"[&+*:not(.large-card)]:mt-2",
"[&+.large-card]:mt-1",
className
)}
>
{children}
</section>
)
}
export default LargeCardList

View File

@@ -0,0 +1,42 @@
import { LearningPathStepType } from "@site/src/providers/LearningPath"
import { Rating } from "docs-ui"
import React from "react"
export type LearningPathFinishType =
| {
type: "rating"
step: Omit<LearningPathStepType, "descriptionJSX"> & {
eventName?: string
}
}
| {
type: "custom"
step: LearningPathStepType & {
descriptionJSX: JSX.Element
}
}
type LearningPathFinishProps = LearningPathFinishType & {
onRating?: () => void
}
const LearningPathFinish: React.FC<LearningPathFinishProps> = ({
type,
step,
onRating,
}) => {
return (
<>
{type === "rating" && (
<Rating event={step.eventName} onRating={onRating} />
)}
{type === "custom" && (
<span className="text-compact-small text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark">
{step.descriptionJSX}
</span>
)}
</>
)
}
export default LearningPathFinish

View File

@@ -0,0 +1,30 @@
import useBaseUrl from "@docusaurus/useBaseUrl"
import clsx from "clsx"
import React from "react"
type LearningPathIconProps = {
className?: string
imgClassName?: string
} & React.AllHTMLAttributes<HTMLDivElement>
const LearningPathIcon: React.FC<LearningPathIconProps> = ({
className = "",
imgClassName = "",
}) => {
return (
<div
className={clsx(
"rounded-full shadow-card-rest dark:shadow-card-rest-dark w-3 h-3 bg-medusa-bg-base dark:bg-medusa-bg-base-dark",
"flex justify-center items-center flex-none",
className
)}
>
<img
src={useBaseUrl("/img/learning-path-img.png")}
className={clsx("rounded-full w-2.5 h-2.5 no-zoom-img", imgClassName)}
/>
</div>
)
}
export default LearningPathIcon

View File

@@ -0,0 +1,35 @@
import React, { useMemo } from "react"
import { getLearningPaths } from "../../../utils/learning-paths"
import LearningPath from ".."
type LearningPathListProps = {
ignore?: string[]
} & React.AllHTMLAttributes<HTMLDivElement>
const LearningPathList: React.FC<LearningPathListProps> = ({ ignore = [] }) => {
const paths = useMemo(() => {
const paths = getLearningPaths()
ignore.forEach((pathName) => {
const pathIndex = paths.findIndex((path) => path.name === pathName)
if (pathIndex !== -1) {
paths.splice(pathIndex, 1)
}
})
return paths
}, [ignore])
return (
<div className="flex flex-col flex-wrap gap-2 mt-1.5">
{paths.map((path, index) => (
<LearningPath
pathName={path.name}
key={index}
className="!mt-0 !mb-0"
/>
))}
</div>
)
}
export default LearningPathList

View File

@@ -0,0 +1,41 @@
import React from "react"
import { useLearningPath } from "../../../../providers/LearningPath"
import { Button } from "docs-ui"
type LearningPathStepActionsType = {
onFinish?: () => void
onClose?: () => void
} & React.AllHTMLAttributes<HTMLDivElement>
const LearningPathStepActions: React.FC<LearningPathStepActionsType> = ({
onFinish,
onClose,
}) => {
const { hasNextStep, nextStep, endPath } = useLearningPath()
const handleFinish = () => {
if (onFinish) {
onFinish()
} else {
endPath()
}
}
return (
<div className="flex gap-0.5 p-1 justify-end items-center">
<Button onClick={onClose}>Close</Button>
{hasNextStep() && (
<Button onClick={nextStep} variant="primary">
Next
</Button>
)}
{!hasNextStep() && (
<Button onClick={handleFinish} variant="primary">
Finish
</Button>
)}
</div>
)
}
export default LearningPathStepActions

View File

@@ -0,0 +1,83 @@
import { useLearningPath } from "@site/src/providers/LearningPath"
import React from "react"
import LearningPathStepActions from "./Actions"
import clsx from "clsx"
import IconCircleDottedLine from "@site/src/theme/Icon/CircleDottedLine"
import Link from "@docusaurus/Link"
import { CheckCircleSolid, CircleMiniSolid } from "@medusajs/icons"
type LearningPathStepsProps = {
onFinish?: () => void
onClose?: () => void
}
const LearningPathSteps: React.FC<LearningPathStepsProps> = ({ ...rest }) => {
const { path, currentStep, goToStep } = useLearningPath()
if (!path) {
return <></>
}
return (
<>
<div className="overflow-auto basis-3/4">
{path.steps.map((step, index) => (
<div
className={clsx(
"border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark",
"relative p-1"
)}
key={index}
>
<div className={clsx("flex items-center gap-1")}>
<div className="w-2 flex-none flex items-center justify-center">
{index === currentStep && (
<IconCircleDottedLine
className={clsx(
"shadow-active dark:shadow-active-dark rounded-full",
"text-ui-fg-interactive"
)}
/>
)}
{index < currentStep && (
<CheckCircleSolid className="text-ui-fg-interactive" />
)}
{index > currentStep && (
<CircleMiniSolid className="text-ui-fg-subtle" />
)}
</div>
<span
className={clsx(
"text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark"
)}
>
{step.title}
</span>
</div>
{index === currentStep && (
<div className={clsx("flex items-center gap-1")}>
<div className="w-2 flex-none"></div>
<div className={clsx("text-medium text-ui-fg-subtle mt-1")}>
{step.descriptionJSX ?? step.description}
</div>
</div>
)}
{index < currentStep && (
<Link
href={step.path}
className={clsx("absolute top-0 left-0 w-full h-full")}
onClick={(e) => {
e.preventDefault()
goToStep(index)
}}
/>
)}
</div>
))}
</div>
<LearningPathStepActions {...rest} />
</>
)
}
export default LearningPathSteps

View File

@@ -0,0 +1,92 @@
import clsx from "clsx"
import React from "react"
import LearningPathIcon from "./Icon"
import { getLearningPath } from "../../utils/learning-paths"
import { useLearningPath } from "../../providers/LearningPath"
import { Button, useNotifications } from "docs-ui"
import { CircleMiniSolid } from "@medusajs/icons"
type LearningPathProps = {
pathName: string
className?: string
} & React.AllHTMLAttributes<HTMLDivElement>
const LearningPath: React.FC<LearningPathProps> = ({
pathName,
className = "",
}) => {
const path = getLearningPath(pathName)
if (!path) {
throw new Error(`Learning path ${pathName} does not exist.`)
}
const { startPath, path: currentPath } = useLearningPath()
const notificationContext = useNotifications()
const handleClick = () => {
if (notificationContext && currentPath?.notificationId) {
notificationContext.removeNotification(currentPath.notificationId)
}
startPath(path)
}
return (
<div
className={clsx(
"rounded shadow-card-rest dark:shadow-card-rest-dark bg-docs-bg-surface dark:bg-docs-bg-surface-dark mt-1.5 mb-4",
className
)}
>
<div
className={clsx(
"flex items-center gap-1 p-1 border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark"
)}
>
<LearningPathIcon />
<div className={clsx("basis-3/4")}>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-large-plus block"
)}
>
{path.label}
</span>
{path.description && (
<span
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark text-compact-medium mt-0.25 inline-block"
)}
>
{path.description}
</span>
)}
</div>
<Button onClick={handleClick} className={clsx("basis-1/4 max-w-fit")}>
Start Path
</Button>
</div>
{path.steps.map((step, index) => (
<div
className={clsx(
"flex items-center p-1 gap-1",
index !== path.steps.length - 1 &&
"border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark"
)}
key={index}
>
<div className={clsx("w-3 flex items-center justify-center")}>
<CircleMiniSolid className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
</div>
<span
className={clsx(
"text-medusa-fg-base dark:text-medusa-fg-base-dark text-compact-medium-plus"
)}
>
{step.title}
</span>
</div>
))}
</div>
)
}
export default LearningPath

View File

@@ -0,0 +1,79 @@
import React from "react"
import { NavbarAction } from "@medusajs/docs"
import Icon from "../../../theme/Icon"
import clsx from "clsx"
import { Button, Tooltip } from "docs-ui"
type NavbarActionsProps = {
items: NavbarAction[]
className?: string
} & React.HTMLAttributes<HTMLDivElement>
const NavbarActions: React.FC<NavbarActionsProps> = ({
items = [],
className = "",
}) => {
return (
<div className={clsx("lg:block hidden", className)}>
{items.map((item, index) => {
// eslint-disable-next-line no-case-declarations
const ItemIconElm = item.Icon
const ItemIcon = item.icon ? Icon[item.icon] : null
switch (item.type) {
case "link":
return (
<Tooltip
text={item.title}
html={item.html}
key={index}
tooltipClassName="!text-compact-x-small-plus"
>
<a
href={item.href}
title={item.title}
className={clsx(
(ItemIcon || ItemIconElm) && "navbar-action-icon-item",
item.className
)}
>
{item.label}
{ItemIconElm}
{ItemIcon && <ItemIcon />}
</a>
</Tooltip>
)
case "button":
return (
<Tooltip
text={item.title}
html={item.html}
key={index}
tooltipClassName="!text-compact-x-small-plus"
>
<Button
className={clsx(item.href && "relative", item.className)}
variant={item.variant || "secondary"}
buttonType={item.buttonType || "default"}
{...item.events}
>
{item.label}
{ItemIconElm}
{ItemIcon && <ItemIcon />}
{item.href && (
<a
href={item.href}
className="absolute top-0 left-0 w-full h-full"
/>
)}
</Button>
</Tooltip>
)
default:
return <></>
}
})}
</div>
)
}
export default NavbarActions

View File

@@ -0,0 +1,29 @@
import Admonition, { Props as AdmonitionProps } from "@theme/Admonition"
import { useQueryStringValue } from "@docusaurus/theme-common/internal"
import React from "react"
type QueryNoteProps = {
query: {
key: string
value?: string
}
admonition: AdmonitionProps
} & React.HTMLAttributes<HTMLDivElement>
const QueryNote: React.FC<QueryNoteProps> = ({
query: { key, value = "" },
admonition,
children,
}) => {
const queryValue = useQueryStringValue(key)
return (
<>
{queryValue === value && (
<Admonition {...admonition}>{children}</Admonition>
)}
</>
)
}
export default QueryNote

View File

@@ -0,0 +1,44 @@
import React from "react"
import Head from "@docusaurus/Head"
import { useLocation } from "@docusaurus/router"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import type { TOCItem } from "@docusaurus/mdx-loader"
type StructuredDataHowToProps = {
toc: readonly TOCItem[]
title: string
}
const StructuredDataHowTo: React.FC<StructuredDataHowToProps> = ({
toc,
title,
}) => {
const location = useLocation()
const {
siteConfig: { url },
} = useDocusaurusContext()
const mainUrl = `${url}/${location.pathname}`
return (
<Head>
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
name: title,
step: [
toc
.filter((item) => item.level === 2)
.map((item) => ({
"@type": "HowToStep",
text: item.value,
url: `${mainUrl}#${item.id}`,
})),
],
})}
</script>
</Head>
)
}
export default StructuredDataHowTo

View File

@@ -0,0 +1,30 @@
import React from "react"
import Details from "../../theme/Details"
import clsx from "clsx"
type TroubleshootingSection = {
title: string
content: React.ReactNode
}
type TroubleshootingProps = {
sections: TroubleshootingSection[]
} & React.AllHTMLAttributes<HTMLDivElement>
const Troubleshooting: React.FC<TroubleshootingProps> = ({ sections }) => {
return (
<>
{sections.map(({ title, content }, index) => (
<Details
summary={title}
key={index}
className={clsx(index !== 0 && "border-t-0")}
>
{content}
</Details>
))}
</>
)
}
export default Troubleshooting

View File

@@ -0,0 +1,27 @@
import React from "react"
import ThemedImage from "@theme/ThemedImage"
type UiIconProps = {
lightIcon: string
darkIcon?: string
alt?: string
}
const UiIcon: React.FC<UiIconProps> = ({
lightIcon,
darkIcon = "",
alt = "",
}) => {
return (
<ThemedImage
alt={alt}
sources={{
light: lightIcon,
dark: darkIcon || lightIcon,
}}
className="align-sub w-[20px] h-[20px]"
/>
)
}
export default UiIcon

View File

@@ -0,0 +1,287 @@
/** This CSS file includes tailwind styling definitions for classes defined by docusaurus **/
html,
body {
@apply h-full;
}
.cards-grid {
@apply grid gap-x-1 auto-rows-fr;
}
.cards-grid.grid-6 {
@apply md:grid-cols-2 grid-cols-1;
}
.cards-grid.grid-4 {
@apply lg:grid-cols-3 md:grid-cols-2 grid-cols-1;
}
h1 + .cards-grid,
h1 + .card-wrapper,
h2 + .cards-grid,
h2 + .card-wrapper,
h3 + .cards-grid,
h3 + .card-wrapper {
@apply mt-1.5;
}
.markdown p + img,
.markdown .alert,
.markdown .code-wrapper {
@apply mb-2;
}
.markdown h2,
.markdown h3,
.markdown p + .code-wrapper,
.markdown p + .tabs-wrapper,
.markdown p + .alert,
.markdown p + section,
video {
@apply mt-2;
}
.theme-code-block {
@apply border border-solid border-medusa-code-border dark:border-medusa-code-border-dark;
}
.markdown p + .card-wrapper {
@apply mt-1.5;
}
.card + p {
@apply mt-2;
}
.card-highlighted {
@apply relative mb-4 bg-medusa-bg-subtle dark:bg-medusa-bg-base-dark hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-subtle-hover-dark;
}
.col.toc-wrapper {
--ifm-col-width: var(--ifm-toc-width);
}
.markdown-doc-wrapper--fluid {
@apply max-w-[inherit];
}
:global(.docusaurus-mt-lg) {
@apply mt-3;
}
:global(#__docusaurus) {
@apply min-h-full flex flex-col;
}
.code-tabs .theme-code-block {
@apply !rounded-t-none !rounded-b
}
[class*=codeLineNumber] {
@apply text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark !pl-0;
}
.prism-code {
@apply xs:max-w-[90%] text-code-body [&_*]:text-code-body xs:after:content-[''] xs:after:rounded xs:after:absolute;
@apply xs:after:right-0 xs:after:top-0 xs:after:w-[calc(10%+24px)] xs:after:h-full xs:after:bg-code-fade dark:xs:after:bg-code-fade-dark;
}
.prism-code:not(:hover)::-webkit-scrollbar-thumb,
.prism-code:not(:hover)::-webkit-scrollbar-track {
@apply xs:invisible;
}
.prism-code:hover::-webkit-scrollbar-thumb,
.prism-code:hover::-webkit-scrollbar-track {
@apply xs:opacity-100
}
.prism-code {
@apply bg-transparent break-words !outline-none;
}
.prism-code div {
@apply !outline-none focus:!outline-none active:!outline-none;
}
.code-tabs {
@apply relative;
}
.sidebar-desktop nav {
--ifm-scrollbar-track-background-color: transparent !important;
--ifm-scrollbar-thumb-background-color: transparent !important;
--ifm-scrollbar-thumb-hover-background-color: transparent !important;
}
.main-wrapper {
@apply max-w-xl mx-auto xl:min-w-xl w-full;
}
.theme-doc-breadcrumbs {
@apply hidden;
}
article {
@apply max-w-[960px];
}
.padding-top--md {
@apply !pt-3;
}
.margin-bottom--lg {
@apply !mb-1;
}
details summary {
@apply cursor-pointer;
}
.reference-table {
@apply !table table-fixed w-full;
}
.reference-table p {
--ifm-paragraph-margin-bottom: 0;
}
.reference-table.table-col-4 th,
.reference-table.table-col-4 td {
@apply [&:nth-child(1)]:!w-1/5 [&:nth-child(2)]:!w-1/5 [&:nth-child(3)]:!w-2/5 [&:nth-child(4)]:!w-1/5;
}
.reference-table th:nth-child(1),
.reference-table td:nth-child(1) {
@apply [&:nth-child(1)]:!w-[30%] [&:nth-child(2)]:!w-[30%] [&:nth-child(3)]:!w-2/5;
}
.reference-table .tooltip-container code {
@apply whitespace-nowrap;
}
.reference-table .theme-code-block span {
@apply !max-w-full !break-words !whitespace-break-spaces;
}
.theme-code-block {
@apply !bg-medusa-code-bg-base dark:!bg-medusa-code-bg-base-dark
}
.reference-table .code-block-numbering {
@apply !block;
}
.container {
@apply !pt-3 !max-w-full;
}
.pagination-nav {
@apply hidden;
}
.theme-doc-footer {
@apply !mt-0 border-0 !border-t border-solid !border-medusa-border-base dark:!border-medusa-border-base-dark;
@apply pt-2;
}
.theme-last-updated {
@apply font-normal not-italic text-compact-small-plus;
}
.theme-last-updated,
.theme-last-updated b {
@apply !font-normal;
}
.medium-zoom-overlay {
@apply z-[400];
}
.medium-zoom-image--opened {
@apply z-[400];
}
details > div {
--docusaurus-details-decoration-color: transparent !important;
}
.row--justify-end {
@apply justify-end;
}
.docs-page-container {
@apply !px-0;
}
.search-result-match {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
@apply bg-medusa-bg-highlight dark:bg-medusa-bg-highlight-dark py-0.25 px-0;
}
.navbar {
@apply z-[399] p-0 border-0 border-b border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
}
html:not(.plugin-redoc) .navbar:not(.navbar-sidebar--show) {
@apply supports-[backdrop-filter]:bg-transparent supports-[backdrop-filter]:backdrop-blur-md;
}
.navbar__link {
@apply text-compact-small-plus;
}
.navbar__brand {
@apply mr-0;
}
.navbar__logo {
@apply w-[82px] h-[20px] lg:w-[20px] lg:h-[20px] flex justify-center items-center !mr-0;
}
.navbar__item {
@apply p-0 lg:!block !hidden;
}
h1 {
@apply !text-h1;
}
h2 {
@apply !text-h2;
}
h3 {
@apply !text-h3;
}
h1, h2, h3, h4, h5, h6 {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
}
.markdown > h1:first-child,
.markdown > h2,
.markdown > h3 {
@apply mb-0.5;
}
.markdown > p {
@apply mb-0.5;
}
.markdown > p img {
@apply mt-0.5;
}
.markdown {
@apply mb-0;
}
*::selection {
@apply bg-medusa-bg-highlight dark:bg-medusa-bg-highlight-dark;
}
.prism-code *::selection, .code-header::selection {
@apply bg-medusa-code-text-highlight;
}

View File

@@ -0,0 +1,208 @@
/* You can override the default Infima variables here. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap');
:root {
/* Base Styles */
--ifm-hr-margin-vertical: theme(margin.2);
--ifm-leading: theme(lineHeight.DEFAULT);
--ifm-list-left-padding: theme(spacing.1);
--font-inter: "Inter";
--font-roboto-mono: "Roboto Mono";
/* Colors */
--ifm-color-primary: theme(colors.medusa.fg.base.DEFAULT);
--ifm-background-color: theme(colors.medusa.bg.base.DEFAULT);
--ifm-background-surface-color: theme(colors.medusa.bg.subtle.DEFAULT);
--ifm-color-content: theme(colors.medusa.fg.subtle.DEFAULT) !important;
--ifm-color-content-secondary: var(--ifm-color-content);
--ifm-base-border-color: theme(colors.medusa.border.base.DEFAULT);
--ifm-strong-border-color: theme(colors.medusa.border.strong.DEFAULT);
--ifm-hr-background-color: theme(colors.medusa.border.base.DEFAULT);
/* Fonts */
--ifm-code-font-size: theme(fontSize.code-label);
--ifm-font-family-base: theme(fontFamily.base);
--ifm-font-family-monospace: theme(fontFamily.monospace);
--ifm-font-size-base: theme(fontSize.medium[0]);
--ifm-line-height-base: theme(fontSize.medium[1].lineHeight);
--ifm-font-weight-base: theme(fontSize.medium[1].fontWeight);
/* Headings */
/** Due to how docusaurus styles headings, the styles are applied on h1, h2, and h3 again in _docusaurus **/
--ifm-color-headers: var(--ifm-color-primary);
--ifm-h1-font-size: theme(fontSize.h1[0]);
--ifm-h1-line-height: theme(fontSize.h1[1].lineHeight);
--ifm-h1-weight: theme(fontSize.h1[1].fontWeight);
--ifm-h2-font-size: theme(fontSize.h2[0]);
--ifm-h2-line-height: theme(fontSize.h2[1].lineHeight);
--ifm-h2-weight: theme(fontSize.h2[1].fontWeight);
--ifm-h3-font-size: theme(fontSize.h3[0]);
--ifm-h3-line-height: theme(fontSize.h3[1].lineHeight);
--ifm-h3-weight: theme(fontSize.h3[1].fontWeight);
--ifm-h4-font-size: var(--ifm-font-size-base);
/* Links */
--ifm-link-color: theme(colors.medusa.fg.interactive.DEFAULT);
--ifm-link-hover-color: theme(colors.medusa.fg.interactive.hover.DEFAULT);
--ifm-link-decoration: none;
--ifm-link-hover-decoration: none;
/* Sidebar */
--ifm-menu-link-padding-vertical: 6px;
--ifm-menu-link-padding-horizontal: theme(margin.1);
--ifm-menu-color: theme(colors.medusa.fg.subtle.DEFAULT);
--ifm-menu-color-active: theme(colors.medusa.fg.base.DEFAULT);
--ifm-menu-color-background-hover: theme(colors.medusa.bg.base.hover.DEFAULT);
--ifm-menu-color-background-active: theme(colors.medusa.bg.base.pressed.DEFAULT);
/* Toc */
--ifm-toc-border-color: theme(colors.medusa.border.base.DEFAULT);
--ifm-toc-link-color: theme(colors.medusa.fg.subtle.DEFAULT);
--ifm-toc-padding-horizontal: theme(padding.1);
/* Navbar */
--ifm-navbar-background-color: var(--ifm-background-color);
--ifm-navbar-shadow: none;
--ifm-navbar-padding-vertical: 12px;
--ifm-navbar-padding-horizontal: theme(padding[1.5]);
--ifm-navbar-item-padding-vertical: 6px;
--ifm-navbar-item-padding-horizontal: theme(padding[1]);
--ifm-navbar-height: theme(height.navbar);
--ifm-navbar-link-hover-color: theme(colors.medusa.fg.base.DEFAULT);
--ifm-navbar-link-color: theme(colors.medusa.fg.subtle.DEFAULT);
/* Inline Code */
--ifm-code-border-radius: theme(borderRadius.DEFAULT);
--ifm-code-padding-horizontal: theme(padding[0.5]);
--ifm-code-background: theme(colors.medusa.tag.neutral.bg.DEFAULT) !important;
/* Code Blocks */
--ifm-pre-background: theme(colors.medusa.code.bg.base.DEFAULT);
--ifm-pre-padding: theme(padding.1);
/* Tabs */
--ifm-tabs-color-active: var(--ifm-color-primary);
/* Tooltip */
--rt-opacity: theme(opacity.100) !important;
--rt-color-dark: theme(colors.medusa.bg.base.DEFAULT) !important;
--rt-color-white: var(--ifm-color-content) !important;
/* Footer */
--ifm-footer-color: theme(colors.medusa.fg.muted.DEFAULT);
--ifm-footer-background-color: transparent;
--ifm-footer-padding-horizontal: 0;
--ifm-footer-link-color: var(--ifm-color-content);
/* Search */
--docsearch-searchbox-background: theme(colors.medusa.bg.subtle.DEFAULT) !important;
--docsearch-searchbox-focus-background: theme(colors.medusa.bg.subtle.hover.DEFAULT) !important;
--docsearch-searchbox-shadow: none !important;
--docsearch-modal-height: 472px !important;
--docsearch-modal-background: theme(colors.medusa.bg.base.DEFAULT) !important;
--docsearch-modal-shadow: theme(boxShadow.modal) !important;
--docsearch-container-background: theme(colors.medusa.bg.overlay.DEFAULT) !important;
--docsearch-key-gradient: theme(colors.medusa.tag.neutral.bg.DEFAULT) !important;
--docsearch-muted-color: theme(colors.medusa.fg.subtle.DEFAULT) !important;
--docsearch-spacing: 12px theme(spacing[1.5]) !important;
--docsearch-highlight-color: theme(colors.medusa.fg.muted.DEFAULT) !important;
--docsearch-text-color: var(--ifm-color-primary) !important;
--docsearch-hit-background: var(--docsearch-modal-background) !important;
--docsearch-hit-height: auto !important;
--docsearch-hit-active-color: var(--docsearch-text-color) !important;
--docsearch-footer-height: 40px !important;
/* Announcement Bar */
--docusaurus-announcement-bar-height: auto !important;
/* Tables */
--ifm-table-border-color: theme(colors.medusa.border.base.DEFAULT);
--ifm-table-head-background: var(--ifm-background-surface-color);
--ifm-table-head-color: var(--ifm-color-primary);
--ifm-table-head-font-weight: theme(fontSize.medium-plus[1].fontWeight);
--ifm-table-stripe-background: var(--ifm-background-surface-color) !important;
}
html[data-theme="dark"] {
/* Colors */
--ifm-background-color: theme(colors.medusa.bg.subtle.dark);
--ifm-background-surface-color: theme(colors.medusa.bg.base.dark);
--ifm-hr-background-color: theme(colors.medusa.border.base.dark);
--ifm-color-primary: theme(colors.medusa.fg.base.dark);
--ifm-color-content: theme(colors.medusa.fg.subtle.dark) !important;
--ifm-base-border-color: theme(colors.medusa.border.base.dark);
--ifm-strong-border-color: theme(colors.medusa.border.strong.dark);
/* Links */
--ifm-link-color: theme(colors.medusa.fg.interactive.DEFAULT);
--ifm-link-hover-color: theme(colors.medusa.fg.interactive.hover.DEFAULT);
/* Sidebar */
--ifm-menu-color: theme(colors.medusa.fg.subtle.dark);
--ifm-menu-color-active: theme(colors.medusa.fg.base.dark);
--ifm-menu-color-background-hover: theme(colors.medusa.bg.subtle.hover.dark);
--ifm-menu-color-background-active: theme(colors.medusa.bg.subtle.pressed.dark);
/* Toc */
--ifm-toc-border-color: theme(colors.medusa.border.base.dark);
--ifm-toc-link-color: theme(colors.medusa.fg.subtle.dark);
/* Navbar */
--ifm-navbar-shadow: none;
--ifm-navbar-link-hover-color: theme(colors.medusa.fg.base.dark);
--ifm-navbar-link-color: theme(colors.medusa.fg.subtle.dark);
/* Inline Code */
--ifm-code-background: theme(colors.medusa.tag.neutral.bg.dark) !important;
/* Code Blocks */
--ifm-pre-background: theme(colors.medusa.code.bg.base.dark);
/* Tooltip */
--rt-color-dark: theme(colors.medusa.bg.base.dark) !important;
/* Footer */
--ifm-footer-color: theme(colors.medusa.fg.muted.dark);
/* Search */
--docsearch-searchbox-background: theme(colors.medusa.bg.subtle.dark) !important;
--docsearch-searchbox-focus-background: theme(colors.medusa.bg.subtle.hover.dark) !important;
--docsearch-modal-background: theme(colors.medusa.bg.base.dark) !important;
--docsearch-modal-shadow: theme(boxShadow.modal-dark) !important;
--docsearch-container-background: theme(colors.medusa.bg.overlay.dark) !important;
--docsearch-key-gradient: theme(colors.medusa.tag.neutral.bg.dark) !important;
--docsearch-muted-color: theme(colors.medusa.fg.subtle.dark) !important;
--docsearch-highlight-color: theme(colors.medusa.fg.muted.dark) !important;
/* Tables */
--ifm-table-border-color: theme(colors.medusa.border.base.dark);
}
@media screen and (min-width: 1441px) {
:root {
/** Table of Content **/
--ifm-toc-width: calc(2 / 12 * 100%);
}
}
@media screen and (max-width: 1440px) {
:root {
/** Table of Content **/
--ifm-toc-width: calc(3 / 12 * 100%);
}
}
@media (min-width: 997px) {
:root {
--docusaurus-announcement-bar-height: 30px;
}
}
.alert {
--ifm-code-background: theme(colors.medusa.tag.neutral.bg.DEFAULT) !important;
}
html[data-theme="dark"] .alert {
--ifm-code-background: theme(colors.medusa.tag.neutral.bg.dark) !important;
}

View File

@@ -0,0 +1,167 @@
.theme-doc-sidebar-container {
--animate-duration: 0.2s;
}
.sidebar-desktop nav {
--ifm-scrollbar-track-background-color: transparent !important;
--ifm-scrollbar-thumb-background-color: transparent !important;
--ifm-scrollbar-thumb-hover-background-color: transparent !important;
}
.menu__list-item:not(:first-child):not(.sidebar-title) {
margin-top: 0 !important;
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) [class*=theme-doc-sidebar-item-],
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) [class*=theme-doc-sidebar-item-] .menu__link--active {
@apply relative;
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) [class*=theme-doc-sidebar-item-]:not(.theme-doc-sidebar-item-link-category)::before,
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category .menu__list-item-collapsible .menu__link--active::before,
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) > .theme-doc-sidebar-item-link > .menu__link--active::before,
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category > .menu__list-item-collapsible::before {
@apply content-[''] h-full w-[1px] absolute left-0.5 top-0 z-[1];
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) [class*=theme-doc-sidebar-item-] .menu__link--active::before {
@apply !h-[20px];
}
.theme-doc-sidebar-item-category-level-1 > .menu__list > .menu__list-item:first-child::before,
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category:first-child .menu__list-item-collapsible::before,
.theme-doc-sidebar-item-category-level-1 > .menu__list > .menu__list-item:last-child::before {
@apply !h-[28px];
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-link:first-child::before,
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category:first-child .menu__list-item-collapsible::before {
@apply !bottom-0 !top-[unset];
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category:first-child .menu__list-item-collapsible {
@apply !pb-[6px];
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category:first-child .menu__list-item-collapsible .menu__link {
@apply !pb-0;
}
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) [class*=theme-doc-sidebar-item-]::before,
.menu__list-item .menu__list:not(.theme-doc-sidebar-menu) .theme-doc-sidebar-item-category:first-child .menu__list-item-collapsible::before {
@apply bg-medusa-border-base dark:bg-medusa-border-base-dark;
}
.theme-doc-sidebar-item-category-level-2 > .menu__list-item-collapsible .menu__link--active::before,
.menu__list:not(.theme-doc-sidebar-menu) > .theme-doc-sidebar-item-link.theme-doc-sidebar-item-link-level-2 > .menu__link--active::before {
@apply bg-medusa-fg-interactive dark:bg-medusa-fg-interactive-dark !z-[2] !top-[6px];
}
.theme-doc-sidebar-item-category-level-2:only-child > .menu__list-item-collapsible .menu__link--active::before,
.menu__list:not(.theme-doc-sidebar-menu) > .theme-doc-sidebar-item-link.theme-doc-sidebar-item-link-level-2:only-child > .menu__link--active::before,
.menu__list:not(.theme-doc-sidebar-menu) > .theme-doc-sidebar-item-link.theme-doc-sidebar-item-link-level-2:first-child > .menu__link--active::before {
@apply !top-0.5;
}
.theme-doc-sidebar-item-link-level-1:not(.sidebar-title):not(.homepage-sidebar-item):not(.sidebar-back-link) {
@apply mb-1;
}
.theme-doc-sidebar-item-link-level-1:not(.sidebar-title):not(.homepage-sidebar-item):not(.sidebar-back-link) .menu__link {
@apply !pl-0;
}
.theme-doc-sidebar-item-category-level-1.sidebar-group-headline {
@apply mb-1;
}
[class*="level-2"]:is([class*="theme-doc-sidebar-item"]) .menu__link,
[class*="level-2"]:is([class*="theme-doc-sidebar-item"]).sidebar-group-divider,
[class*="level-3"]:is([class*="theme-doc-sidebar-item"]) .menu__link,
[class*="level-3"]:is([class*="theme-doc-sidebar-item"]).sidebar-group-divider,
[class*="level-4"]:is([class*="theme-doc-sidebar-item"]) .menu__link,
[class*="level-4"]:is([class*="theme-doc-sidebar-item"]).sidebar-group-divider {
@apply py-[6px];
}
[class*="level-2"]:is([class*="theme-doc-sidebar-item"]) .menu__link,
[class*="level-2"]:is([class*="theme-doc-sidebar-item"]).sidebar-group-divider {
@apply pl-2;
}
[class*="level-3"]:is([class*="theme-doc-sidebar-item"]) .menu__link,
[class*="level-3"]:is([class*="theme-doc-sidebar-item"]).sidebar-group-divider {
@apply pl-3;
}
[class*="level-4"]:is([class*="theme-doc-sidebar-item"]) .menu__link,
[class*="level-4"]:is([class*="theme-doc-sidebar-item"]).sidebar-group-divider {
@apply pl-4;
}
.homepage-sidebar-item:is([class*="level-1"]):is([class*="theme-doc-sidebar-item"]) > .menu__link,
.homepage-sidebar-item:is([class*="level-1"]):is([class*="theme-doc-sidebar-item"]) > .menu__list-item-collapsible > .menu__link {
@apply !pl-0.5;
}
.menu__list-item-collapsible {
@apply !rounded;
}
.menu__list-item-collapsible--active {
@apply !bg-transparent;
}
.menu__list-item-collapsible .menu__link--sublist:hover {
@apply !bg-medusa-bg-base-hover dark:!bg-medusa-bg-subtle-hover-dark;
}
.menu__list:not(.theme-doc-sidebar-menu) > .theme-doc-sidebar-item-link:last-child > .menu__link--active::before {
@apply !top-0.25;
}
.menu__link--sublist-caret:after {
@apply content-none;
}
.menu__link {
@apply cursor-pointer !rounded text-compact-small-plus;
}
.theme-doc-sidebar-item-category-level-1 > .menu__list {
@apply mb-0.25;
}
.menu__list:not(.theme-doc-sidebar-menu),
.theme-doc-sidebar-item-category-level-1 > .menu__list {
@apply !pl-0;
}
.menu__list .menu__list {
@apply !mt-0;
}
/** General sidebar styles **/
.theme-doc-sidebar-container {
@apply z-[398] border-0 border-r border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
}
/** Mobile Sidebar **/
.navbar-sidebar__back {
@apply bg-transparent w-fit top-[unset] pl-0 ml-0 mb-1 text-compact-small-plus;
}
.theme-doc-sidebar-item-link {
@apply flex items-center;
}
/** Redocly sidebar **/
.redocusaurus .menu-content > div > ul > li > ul label:not([type=tag]):not(.active) {
@apply text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark;
@apply hover:text-medusa-fg-base dark:hover:text-medusa-fg-base-dark;
}
.redocusaurus .menu-content > div > ul > li > ul label:not([type=tag]).active {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
}

View File

@@ -0,0 +1,60 @@
.theme-doc-toc-desktop {
@apply pt-3;
}
.theme-doc-toc-desktop .table-of-contents {
@apply !border-l-0 !p-0 text-[12px] relative overflow-y-hidden;
@apply before:bg-toc dark:before:bg-toc-dark;
@apply before:bg-no-repeat before:bg-[length:14px_10px] before:bg-[center_left];
@apply before:pl-[22px] before:content-['On_this_page'] before:pb-0 text-compact-small-plus;
@apply after:content-[''] after:absolute after:left-0 after:top-2.5;
@apply after:h-full after:w-[1px] after:bg-medusa-border-base dark:after:bg-medusa-border-base-dark;
}
.theme-doc-toc-desktop .table-of-contents .table-of-contents__link {
@apply pl-[11px];
}
.theme-doc-toc-desktop .table-of-contents > li {
@apply first:mt-1 [&:not(:first-child)]:mt-0 last:mb-0 [&:not(:last-child)]:mb-0.25;
}
.theme-doc-toc-desktop .table-of-contents li a + ul {
@apply mt-0.25;
}
.theme-doc-toc-desktop .table-of-contents__link {
@apply relative text-compact-x-small-plus;
@apply hover:text-medusa-fg-base dark:hover:text-medusa-fg-base-dark;
}
.theme-doc-toc-desktop .table-of-contents__link:hover code {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
}
.theme-doc-toc-desktop .table-of-contents__link--active {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
@apply after:content-[''] after:absolute after:left-0 after:top-0;
@apply after:h-full after:w-[1px] after:bg-medusa-fg-base dark:after:bg-medusa-fg-base-dark;
@apply z-[1];
}
.theme-doc-toc-desktop .table-of-contents__link--active code {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark;
}
.theme-doc-toc-desktop .table-of-contents li {
@apply mx-0;
}
.theme-doc-toc-desktop .table-of-contents ul li {
@apply mt-0;
}
.table-of-contents ul {
@apply pl-0;
}
.table-of-contents ul .table-of-contents__link {
@apply !pl-[27px];
}

View File

@@ -0,0 +1,10 @@
.react-tooltip {
@apply !border !border-solid !border-medusa-border-base dark:!border-medusa-border-base-dark;
@apply !rounded !text-compact-x-small-plus !shadow-tooltip dark:!shadow-tooltip-dark;
@apply !py-0.4 !px-1 lg:block hidden;
}
.react-tooltip-arrow {
@apply hidden;
}

View File

@@ -0,0 +1,185 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body[data-modal="opened"] {
@apply !overflow-hidden;
}
mark {
@apply bg-medusa-bg-highlight dark:bg-medusa-bg-highlight-dark;
@apply text-medusa-fg-interactive dark:text-medusa-fg-interactive-dark;
}
}
@layer components {
.sidebar-title {
@apply !m-0 !py-1 !px-0;
@apply lg:first:top-0 lg:[&:not(:first-child)]:top-[20px];
}
.sidebar-title .menu__link {
@apply !px-0;
}
.sidebar-title .menu__link,
.sidebar-title span {
@apply !text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark;
}
.sidebar-title .menu__link--active,
.sidebar-title .menu__link:hover {
@apply !bg-transparent;
}
.sidebar-group-headline {
@apply mt-1 py-[6px] px-0;
}
.sidebar-group-headline:not(.theme-doc-sidebar-item-category-level-1) {
@apply mb-[6px];
}
.sidebar-group-headline > .menu__list-item-collapsible > .menu__link {
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark !p-0;
}
.sidebar-group-headline > .menu__list-item-collapsible > .menu__link:not([href]) {
@apply cursor-default
}
.sidebar-group-headline > .menu__list-item-collapsible > .menu__link:hover,
.sidebar-group-headline .menu__list-item-collapsible:hover {
@apply !bg-transparent;
}
.sidebar-group-headline > .menu__link,
.sidebar-group-headline > .menu__list-item-collapsible > .menu__link {
@apply font-semibold;
}
.sidebar-group-divider {
@apply pb-[6px] uppercase text-medusa-fg-muted dark:text-medusa-fg-muted-dark text-compact-x-small-plus;
@apply first:pt-[6px] [&:not(:first-child)]:!pt-1;
}
.sidebar-divider-line {
@apply h-[1px] w-full bg-medusa-border-base dark:bg-medusa-border-base-dark my-1 mx-0;
}
.sidebar-back-link {
@apply cursor-pointer lg:top-0;
}
.sidebar-back-link,
.sidebar-title {
@apply lg:sticky lg:bg-medusa-bg-base dark:lg:bg-medusa-bg-subtle-dark lg:z-[100];
}
.sidebar-back-link .menu__link {
@apply !p-0 hover:!bg-transparent hover:text-medusa-fg-base dark:hover:text-medusa-fg-base-dark;
}
.sidebar-back-link .menu__link,
.sidebar-back-link span {
@apply text-compact-small-plus;
}
.sidebar-back-link .sidebar-item-icon {
@apply mr-0.5;
}
.sidebar-soon-link {
@apply pointer-events-none [&_*]:text-medusa-fg-disabled dark:[&_*]:text-medusa-fg-disabled-dark;
}
.sidebar-badge-wrapper {
@apply flex justify-between items-center;
}
.search-page-input {
@apply rounded border border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
@apply font-base text-medium p-0.75 w-full bg-medusa-bg-subtle dark:bg-medusa-bg-subtle-dark;
@apply text-medusa-fg-base dark:text-medusa-fg-base-dark mb-1;
@apply transition-[border] duration-200 ease-ease;
@apply focus:border-medusa-border-base dark:focus:border-medusa-border-base-dark;
@apply focus:outline-none !shadow-none;
}
.code-header {
@apply py-0.75 px-1 bg-medusa-code-bg-header dark:bg-medusa-code-bg-header-dark text-medusa-code-text-subtle dark:text-medusa-code-text-subtle;
@apply flex justify-between items-center;
@apply rounded-tl rounded-tr rounded-br-none rounded-bl-none border-b-0;
@apply border border-solid border-medusa-code-border dark:border-medusa-code-border-dark border-b-0;
}
.no-scrollbar::-webkit-scrollbar {
@apply hidden;
}
.animate__fastest {
--animate-duration: 0.2s;
}
/* .btn-secondary {
@apply inline-flex flex-row justify-center items-center;
@apply py-[5px] px-0.75 rounded-sm cursor-pointer;
@apply bg-button-neutral bg-medusa-button-neutral dark:bg-button-neutral-dark dark:bg-medusa-button-neutral-dark;
@apply hover:bg-medusa-button-neutral-hover hover:bg-no-image dark:hover:bg-medusa-button-neutral-hover-dark hover:no-underline;
@apply active:bg-medusa-button-neutral-pressed active:bg-no-image dark:active:bg-medusa-button-neutral-pressed-dark;
@apply focus:bg-medusa-button-neutral-pressed focus:bg-no-image dark:focus:bg-medusa-button-neutral-pressed-dark;
@apply disabled:!bg-no-image disabled:bg-medusa-bg-disabled dark:disabled:bg-medusa-bg-disabled-dark;
@apply disabled:cursor-not-allowed;
@apply border border-solid border-medusa-border-base dark:border-medusa-border-base-dark;
@apply text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark;
@apply hover:text-medusa-fg-base hover:dark:text-medusa-fg-base-dark;
@apply shadow-button-neutral focus:shadow-button-neutral-focused active:shadow-button-neutral-focused transition-shadow;
@apply dark:shadow-button-neutral dark:focus:shadow-button-neutral-focused dark:active:shadow-button-neutral-focused;
@apply select-none;
}
.btn-primary {
@apply inline-flex flex-row justify-center items-center;
@apply py-[5px] px-0.75 rounded-sm cursor-pointer;
@apply bg-button-inverted bg-medusa-button-inverted dark:bg-button-inverted-dark dark:bg-medusa-button-inverted-dark;
@apply hover:bg-medusa-button-inverted-hover hover:bg-no-image dark:hover:bg-medusa-button-inverted-hover-dark hover:no-underline;
@apply active:bg-medusa-button-inverted-pressed active:bg-no-image dark:active:bg-medusa-button-inverted-pressed-dark;
@apply focus:bg-medusa-button-inverted-pressed focus:bg-no-image dark:focus:bg-medusa-button-inverted-pressed-dark;
@apply shadow-button-colored active:shadow-button-colored-focused focus:shadow-button-colored-focused transition-shadow;
@apply dark:shadow-button-colored-dark dark:active:shadow-button-colored-focused-dark dark:focus:shadow-button-colored-focused-dark;
@apply disabled:!bg-no-image disabled:bg-medusa-button-disabled dark:disabled:bg-medusa-button-disabled-dark;
@apply disabled:cursor-not-allowed disabled:border-medusa-border-base dark:disabled:border-medusa-border-base-dark;
@apply text-compact-small-plus text-medusa-fg-on-inverted dark:text-medusa-fg-on-inverted-dark;
@apply disabled:text-medusa-fg-disabled dark:disabled:text-medusa-fg-disabled-dark;
@apply border border-medusa-border-loud dark:border-medusa-border-loud-dark;
@apply select-none;
} */
.navbar-action-icon-item {
@apply lg:bg-button-neutral lg:bg-medusa-button-neutral lg:dark:bg-button-neutral-dark lg:dark:bg-medusa-button-neutral-dark;
@apply lg:hover:bg-medusa-button-neutral-hover lg:hover:bg-no-image lg:dark:hover:bg-medusa-button-neutral-hover-dark lg:hover:no-underline;
@apply lg:active:bg-medusa-button-neutral-pressed lg:active:bg-no-image lg:dark:active:bg-medusa-button-neutral-pressed-dark;
@apply lg:focus:bg-medusa-button-neutral-pressed lg:focus:bg-no-image lg:dark:focus:bg-medusa-button-neutral-pressed-dark;
@apply lg:lg:border lg:border-solid lg:border-medusa-border-base lg:dark:border-medusa-border-base-dark rounded;
@apply w-2 h-2 flex justify-center items-center cursor-pointer;
}
.btn-clear {
@apply bg-transparent shadow-none border-0 outline-none cursor-pointer;
}
}
@layer utilities {
.clip {
clip-path: inset(0);
}
}
@import url('./_variables.css');
@import url('./_docusaurus.css');
@import url('./components/sidebar.css');
@import url('./components/toc.css');
@import url('./components/tooltip.css');

View File

@@ -0,0 +1,95 @@
import React, { useEffect } from "react"
import { useLearningPath } from "../providers/LearningPath"
import LearningPathSteps from "../components/LearningPath/Steps"
import LearningPathFinish from "../components/LearningPath/Finish"
import LearningPathIcon from "../components/LearningPath/Icon"
import { useNotifications } from "docs-ui"
const useCurrentLearningPath = () => {
const { path, currentStep, updatePath, endPath } = useLearningPath()
const step = path?.steps[currentStep]
const {
addNotification,
generateId,
removeNotification,
updateNotification,
} = useNotifications()
// used when a notification closed (finished or not)
const handleClose = (notificationId: string, shouldEndPath = true) => {
if (shouldEndPath) {
setTimeout(() => {
endPath()
}, 500)
}
removeNotification(notificationId)
}
// used when the learning path is completely finished
// shows the finish step, if the path has any
const handleFinish = (notificationId: string) => {
if (path.finish) {
updateNotification(notificationId, {
title: path.finish.step.title,
text: path.finish.step.description,
type: "custom",
layout: "default",
CustomIcon: (
<LearningPathIcon
className="!w-2 !h-2"
imgClassName="!w-1.5 !h-1.5"
/>
),
children: (
<LearningPathFinish
{...path.finish}
onRating={() =>
setTimeout(() => {
handleClose(notificationId, false)
}, 1500)
}
/>
),
})
endPath()
} else {
handleClose(notificationId)
}
}
const LearningStep = (notificationId: string) => {
return <LearningPathSteps onFinish={() => handleFinish(notificationId)} />
}
// create a notification when a path is initialized
useEffect(() => {
if (path && !path.notificationId) {
const id = generateId()
addNotification({
title: path.label,
text: step?.description,
onClose: () => handleClose(id),
layout: "empty",
id,
children: LearningStep(id),
className: "flex flex-col",
})
updatePath({
notificationId: id,
})
}
}, [path])
// update an existing notification when the step changes
useEffect(() => {
if (path && path.notificationId && step) {
updateNotification(path.notificationId, {
text: step?.description,
children: LearningStep(path.notificationId),
})
}
}, [step])
}
export default useCurrentLearningPath

View File

@@ -0,0 +1,35 @@
import React, { useEffect, useState } from "react"
import { useQueryStringValue } from "@docusaurus/theme-common/internal"
import { Rating, useAnalytics, useNotifications } from "docs-ui"
const useOnboarding = () => {
const isOnboarding = useQueryStringValue("ref") === "onboarding"
const [showNotification, setShowNotification] = useState(isOnboarding)
const { addNotification, removeNotification, generateId } = useNotifications()
const { track } = useAnalytics()
useEffect(() => {
if (isOnboarding) {
track("finished_onboarding")
const id = generateId()
addNotification({
title: "Thank you for installing Medusa!",
text: "Please rate your onboarding experience",
type: "success",
show: showNotification,
setShow: setShowNotification,
id,
children: (
<Rating
event="rating_onboarding"
onRating={() => {
setTimeout(() => removeNotification(id), 1500)
}}
/>
),
})
}
}, [])
}
export default useOnboarding

View File

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

View File

@@ -0,0 +1,30 @@
import { AnalyticsProvider, ModalProvider, NotificationProvider } from "docs-ui"
import React from "react"
import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
import SearchProvider from "../Search"
import LearningPathProvider from "../LearningPath"
type DocsProvidersProps = {
children?: React.ReactNode
}
const DocsProviders = ({ children }: DocsProvidersProps) => {
const {
analytics: { apiKey },
} = useThemeConfig() as ThemeConfig
return (
<AnalyticsProvider writeKey={apiKey}>
<ModalProvider>
<SearchProvider>
<LearningPathProvider>
<NotificationProvider>{children}</NotificationProvider>
</LearningPathProvider>
</SearchProvider>
</ModalProvider>
</AnalyticsProvider>
)
}
export default DocsProviders

View File

@@ -0,0 +1,256 @@
import useIsBrowser from "@docusaurus/useIsBrowser"
import { getLearningPath } from "@site/src/utils/learning-paths"
import React, { createContext, useContext, useEffect, useState } from "react"
import { useHistory } from "@docusaurus/router"
import { LearningPathFinishType } from "@site/src/components/LearningPath/Finish"
import { useAnalytics } from "docs-ui"
export type LearningPathType = {
name: string
label: string
description?: string
steps: LearningPathStepType[]
finish?: LearningPathFinishType
notificationId?: string
}
export type LearningPathStepType = {
title?: string
description?: string
descriptionJSX?: JSX.Element
path?: string
}
export type LearningPathContextType = {
path?: LearningPathType
setPath: (value: LearningPathType) => void
currentStep: number
setCurrentStep: (value: number) => void
startPath: (path: LearningPathType) => void
updatePath: (data: Pick<LearningPathType, "notificationId">) => void
endPath: () => void
nextStep: () => void
hasNextStep: () => boolean
previousStep: () => void
hasPreviousStep: () => boolean
goToStep: (stepIndex: number) => void
isCurrentPath: () => boolean
goToCurrentPath: () => void
}
type LearningPathProviderProps = {
children?: React.ReactNode
}
const LearningPathContext = createContext<LearningPathContextType | null>(null)
const LearningPathProvider: React.FC<LearningPathProviderProps> = ({
children,
}) => {
const [path, setPath] = useState<LearningPathType | null>(null)
const [currentStep, setCurrentStep] = useState(-1)
const isBrowser = useIsBrowser()
const history = useHistory()
const { track } = useAnalytics()
const startPath = (path: LearningPathType) => {
setPath(path)
setCurrentStep(-1)
if (isBrowser) {
localStorage.setItem(
"learning-path",
JSON.stringify({
pathName: path.name,
currentStep: -1,
})
)
}
track(`learning_path_${path.name}`, {
url: history.location.pathname,
state: `start`,
})
}
useEffect(() => {
if (path && currentStep === -1) {
nextStep()
}
}, [path])
const endPath = () => {
const didFinish = currentStep === path.steps.length - 1
const reachedIndex = currentStep === -1 ? 0 : currentStep
track(`learning_path_${path.name}`, {
url: history.location.pathname,
state: !didFinish ? `closed` : `end`,
reachedStep:
path.steps[reachedIndex]?.title ||
path.steps[reachedIndex]?.description ||
path.steps[reachedIndex]?.descriptionJSX ||
reachedIndex,
})
setPath(null)
setCurrentStep(-1)
if (isBrowser) {
localStorage.removeItem("learning-path")
}
}
const hasNextStep = () => currentStep !== path?.steps.length - 1
const nextStep = () => {
if (!path || !hasNextStep()) {
return
}
const nextStepIndex = currentStep + 1
setCurrentStep(nextStepIndex)
const newPath = path.steps[nextStepIndex].path
if (isBrowser) {
localStorage.setItem(
"learning-path",
JSON.stringify({
pathName: path.name,
currentStep: nextStepIndex,
})
)
}
if (history.location.pathname !== newPath) {
history.push(newPath)
}
}
const hasPreviousStep = () => currentStep > 0
const previousStep = () => {
if (!path || !hasPreviousStep()) {
return
}
const previousStepIndex = currentStep - 1
setCurrentStep(previousStepIndex)
const newPath = path.steps[previousStepIndex].path
if (isBrowser) {
localStorage.setItem(
"learning-path",
JSON.stringify({
pathName: path.name,
currentStep: previousStepIndex,
})
)
}
if (history.location.pathname !== newPath) {
history.push(newPath)
}
}
const goToStep = (stepIndex: number) => {
if (!path || stepIndex >= path.steps.length) {
return
}
setCurrentStep(stepIndex)
const newPath = path.steps[stepIndex].path
if (isBrowser) {
localStorage.setItem(
"learning-path",
JSON.stringify({
pathName: path.name,
currentStep: stepIndex,
})
)
}
if (history.location.pathname !== newPath) {
history.push(newPath)
}
}
const isCurrentPath = () => {
if (!path || currentStep === -1) {
return false
}
return history.location.pathname === path.steps[currentStep].path
}
const goToCurrentPath = () => {
if (!path || currentStep === -1) {
return
}
history.push(path.steps[currentStep].path)
}
const updatePath = (data: Pick<LearningPathType, "notificationId">) => {
setPath({
...path,
...data,
})
}
const initPath = () => {
if (isBrowser) {
// give query parameters higher precedence over local storage
const queryPathName = new URLSearchParams(history.location.search).get(
"path"
)
const queryPath = getLearningPath(queryPathName)
if (queryPath) {
startPath(queryPath)
} else {
const storedPath = localStorage.getItem("learning-path")
if (storedPath) {
const storedPathParsed = JSON.parse(storedPath)
const currentPath = getLearningPath(storedPathParsed?.pathName)
if (currentPath) {
setPath(currentPath)
setCurrentStep(storedPathParsed?.currentStep || 0)
}
}
}
}
}
useEffect(() => {
if (isBrowser && !path) {
initPath()
}
}, [isBrowser])
return (
<LearningPathContext.Provider
value={{
path,
setPath,
currentStep,
setCurrentStep,
startPath,
updatePath,
endPath,
nextStep,
hasNextStep,
previousStep,
hasPreviousStep,
goToStep,
isCurrentPath,
goToCurrentPath,
}}
>
{children}
</LearningPathContext.Provider>
)
}
export default LearningPathProvider
export const useLearningPath = () => {
const context = useContext(LearningPathContext)
if (!context) {
throw new Error(
"useLearningPath must be used within a LearningPathProvider"
)
}
return context
}

View File

@@ -0,0 +1,71 @@
import React, { useEffect, useState } from "react"
import { SearchProvider as UiSearchProvider, checkArraySameElms } from "docs-ui"
import { ThemeConfig } from "@medusajs/docs"
import { useThemeConfig } from "@docusaurus/theme-common"
import { useLocalPathname } from "@docusaurus/theme-common/internal"
type SearchProviderProps = {
children: React.ReactNode
}
const SearchProvider = ({ children }: SearchProviderProps) => {
const [defaultFilters, setDefaultFilters] = useState<string[]>([])
const { algoliaConfig: algolia } = useThemeConfig() as ThemeConfig
const currentPath = useLocalPathname()
useEffect(() => {
let resultFilters = []
algolia.defaultFiltersByPath.some((filtersByPath) => {
if (currentPath.startsWith(filtersByPath.path)) {
resultFilters = filtersByPath.filters
}
})
if (!resultFilters.length && algolia.defaultFilters) {
resultFilters = algolia.defaultFilters
}
if (!checkArraySameElms(defaultFilters, resultFilters)) {
setDefaultFilters(resultFilters)
}
}, [currentPath])
return (
<UiSearchProvider
algolia={{
appId: algolia.appId,
apiKey: algolia.apiKey,
mainIndexName: algolia.indexNames.docs,
indices: Object.values(algolia.indexNames),
}}
searchProps={{
filterOptions: algolia.filters,
suggestions: [
{
title: "Getting started? Try one of the following terms.",
items: [
"Install Medusa with create-medusa-app",
"Next.js quickstart",
"Admin dashboard quickstart",
"Commerce modules",
"Medusa architecture",
],
},
{
title: "Developing with Medusa",
items: [
"Recipes",
"How to create endpoints",
"How to create an entity",
"How to create a plugin",
"How to create an admin widget",
],
},
],
}}
initialDefaultFilters={defaultFilters}
>
{children}
</UiSearchProvider>
)
}
export default SearchProvider

View File

@@ -0,0 +1,102 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react"
import { prefersReducedMotion } from "@docusaurus/theme-common"
type SidebarContextType = {
hasSidebar: boolean
hiddenSidebar: boolean
setHiddenSidebar: (value: boolean) => void
hiddenSidebarContainer: boolean
setHiddenSidebarContainer: (value: boolean) => void
floatingSidebar: boolean
setFloatingSidebar: (value: boolean) => void
onCollapse: () => void
}
const SidebarContext = createContext<SidebarContextType | null>(null)
type SidebarProviderProps = {
sidebarName?: string
children?: React.ReactNode
}
const SidebarProvider: React.FC<SidebarProviderProps> = ({
sidebarName,
children,
}) => {
const [hiddenSidebar, setHiddenSidebar] = useState(false)
const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false)
const [floatingSidebar, setFloatingSidebar] = useState(false)
const toggleSidebar = useCallback(() => {
if (hiddenSidebar) {
setHiddenSidebar(false)
}
// onTransitionEnd won't fire when sidebar animation is disabled
// fixes https://github.com/facebook/docusaurus/issues/8918
if (!hiddenSidebar && prefersReducedMotion()) {
setHiddenSidebar(true)
}
setHiddenSidebarContainer((value) => !value)
}, [setHiddenSidebarContainer, hiddenSidebar])
useEffect(() => {
function isEditingContent(event: KeyboardEvent) {
const element = event.target as HTMLElement
const tagName = element.tagName
return (
element.isContentEditable ||
tagName === "INPUT" ||
tagName === "SELECT" ||
tagName === "TEXTAREA"
)
}
function sidebarShortcut(e: KeyboardEvent) {
if (
(e.metaKey || e.ctrlKey) &&
e.key.toLowerCase() === "i" &&
!isEditingContent(e)
) {
e.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", sidebarShortcut)
return () => {
window.removeEventListener("keydown", sidebarShortcut)
}
})
return (
<SidebarContext.Provider
value={{
hasSidebar: sidebarName !== undefined,
hiddenSidebar,
setHiddenSidebar,
hiddenSidebarContainer,
setHiddenSidebarContainer,
floatingSidebar,
setFloatingSidebar,
onCollapse: toggleSidebar,
}}
>
{children}
</SidebarContext.Provider>
)
}
export default SidebarProvider
export const useSidebar = () => {
const context = useContext(SidebarContext)
return context
}

View File

@@ -0,0 +1,188 @@
import React, { type ReactNode } from "react"
import clsx from "clsx"
import Translate from "@docusaurus/Translate"
import type { Props } from "@theme/Admonition"
import {
ExclamationCircleSolid,
InformationCircleSolid,
LightBulbSolid,
} from "@medusajs/icons"
function NoteIcon() {
return (
<InformationCircleSolid className="inline-block mr-0.125 text-medusa-fg-interactive-dark" />
)
}
function TipIcon() {
return (
<LightBulbSolid className="inline-block mr-0.125 text-medusa-tag-orange-icon-dark" />
)
}
function DangerIcon() {
return (
<ExclamationCircleSolid className="inline-block mr-0.125 text-medusa-fg-error dark:text-medusa-fg-error-dark" />
)
}
function InfoIcon() {
return NoteIcon()
}
function CautionIcon() {
return DangerIcon()
}
type AdmonitionConfig = {
iconComponent: React.ComponentType
infimaClassName: string
label: ReactNode
}
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
const AdmonitionConfigs: Record<Props["type"], AdmonitionConfig> = {
note: {
infimaClassName: "secondary",
iconComponent: NoteIcon,
label: (
<Translate
id="theme.admonition.note"
description="The default label used for the Note admonition (:::note)"
>
note
</Translate>
),
},
tip: {
infimaClassName: "success",
iconComponent: TipIcon,
label: (
<Translate
id="theme.admonition.tip"
description="The default label used for the Tip admonition (:::tip)"
>
tip
</Translate>
),
},
danger: {
infimaClassName: "danger",
iconComponent: DangerIcon,
label: (
<Translate
id="theme.admonition.danger"
description="The default label used for the Danger admonition (:::danger)"
>
danger
</Translate>
),
},
info: {
infimaClassName: "info",
iconComponent: InfoIcon,
label: (
<Translate
id="theme.admonition.info"
description="The default label used for the Info admonition (:::info)"
>
info
</Translate>
),
},
caution: {
infimaClassName: "warning",
iconComponent: CautionIcon,
label: (
<Translate
id="theme.admonition.caution"
description="The default label used for the Caution admonition (:::caution)"
>
caution
</Translate>
),
},
}
// Legacy aliases, undocumented but kept for retro-compatibility
const aliases = {
secondary: "note",
important: "info",
success: "tip",
warning: "danger",
} as const
function getAdmonitionConfig(unsafeType: string): AdmonitionConfig {
const type =
(aliases as { [key: string]: Props["type"] })[unsafeType] ?? unsafeType
const config = (AdmonitionConfigs as { [key: string]: AdmonitionConfig })[
type
]
if (config) {
return config
}
console.warn(
`No admonition config found for admonition type "${type}". Using Info as fallback.`
)
return AdmonitionConfigs.info
}
// Workaround because it's difficult in MDX v1 to provide a MDX title as props
// See https://github.com/facebook/docusaurus/pull/7152#issuecomment-1145779682
function extractMDXAdmonitionTitle(children: ReactNode): {
mdxAdmonitionTitle: ReactNode | undefined
rest: ReactNode
} {
const items = React.Children.toArray(children)
const mdxAdmonitionTitle = items.find(
(item) =>
React.isValidElement(item) &&
(item.props as { mdxType: string } | null)?.mdxType ===
"mdxAdmonitionTitle"
)
const rest = <>{items.filter((item) => item !== mdxAdmonitionTitle)}</>
return {
mdxAdmonitionTitle,
rest,
}
}
function processAdmonitionProps(props: Props): Props {
const { mdxAdmonitionTitle, rest } = extractMDXAdmonitionTitle(props.children)
return {
...props,
title: props.title ?? mdxAdmonitionTitle,
children: rest,
}
}
export default function Admonition(props: Props): JSX.Element {
const { children, type, icon: iconProp } = processAdmonitionProps(props)
const typeConfig = getAdmonitionConfig(type)
const { iconComponent: IconComponent } = typeConfig
const icon = iconProp ?? <IconComponent />
return (
<div
className={clsx(
"p-1 border border-solid border-medusa-border-base dark:border-medusa-border-base-dark rounded",
"bg-medusa-bg-subtle dark:bg-medusa-bg-base-dark shadow-none",
"[&_a]:no-underline [&_a]:text-medusa-fg-interactive dark:[&_a]:text-medusa-fg-interactive-dark hover:[&_a]:text-medusa-fg-interactive-hover dark:hover:[&_a]:text-medusa-fg-interactive-hover-dark",
"mb-2 alert"
)}
>
<div className={clsx("flex")}>
<span className={clsx("inline-block h-1.5 w-1.5 mr-1")}>{icon}</span>
<div
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-medium flex-1 [&>*:last-child]:mb-0",
"[&>p>code]:px-0.5 [&>p>code]:text-code-label"
)}
>
{children}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
import React from "react"
import clsx from "clsx"
import { translate } from "@docusaurus/Translate"
import IconClose from "../../Icon/Close"
import type { Props } from "@theme/AnnouncementBar/CloseButton"
export default function AnnouncementBarCloseButton(
props: Props
): JSX.Element | null {
return (
<button
type="button"
aria-label={translate({
id: "theme.AnnouncementBar.closeButtonAriaLabel",
message: "Close",
description: "The ARIA label for close button of announcement bar",
})}
{...props}
className={clsx(
"p-0 leading-[0] self-start opacity-100 hover:opacity-100",
"bg-transparent border-0 cursor-pointer",
props.className
)}
>
<IconClose
width={20}
height={20}
strokeWidth={1.5}
className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark"
/>
</button>
)
}

View File

@@ -0,0 +1,29 @@
import React from "react"
import clsx from "clsx"
import { useThemeConfig } from "@docusaurus/theme-common"
import type { Props } from "@theme/AnnouncementBar/Content"
export default function AnnouncementBarContent(
props: Props
): JSX.Element | null {
const { announcementBar } = useThemeConfig()
const { content } = announcementBar!
return (
<div
className={clsx(
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"text-compact-x-small-plus",
props.className
)}
>
<div
{...props}
className={clsx("text-medusa-fg-base dark:text-medusa-fg-base-dark")}
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: content }}
/>
<span>Read more</span>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import React from "react"
import { useThemeConfig } from "@docusaurus/theme-common"
import { useAnnouncementBar } from "@docusaurus/theme-common/internal"
import AnnouncementBarCloseButton from "@theme/AnnouncementBar/CloseButton"
import AnnouncementBarContent from "@theme/AnnouncementBar/Content"
import clsx from "clsx"
import { Bordered } from "docs-ui"
import { BellAlertSolid } from "@medusajs/icons"
export default function AnnouncementBar(): JSX.Element | null {
const { announcementBar } = useThemeConfig()
const { isActive, close } = useAnnouncementBar()
if (!isActive) {
return null
}
const { isCloseable, id } = announcementBar!
return (
<div
className={clsx(
"relative flex items-center h-auto bg-medusa-bg-subtle dark:bg-medusa-bg-base-dark p-0.75",
"rounded mx-1.5 mb-1 shadow-card-rest dark:shadow-card-rest-dark",
"transition-all duration-200 ease-ease",
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-base-hover-dark",
"print:hidden"
)}
>
<Bordered wrapperClassName="mr-0.75">
<div
className={clsx(
"p-[6px] flex justify-center items-center",
"rounded-xs bg-medusa-bg-component dark:bg-medusa-bg-component-dark"
)}
>
<BellAlertSolid className="text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark" />
</div>
</Bordered>
<AnnouncementBarContent className={clsx("flex-1")} />
{isCloseable && (
<AnnouncementBarCloseButton
onClick={close}
className={clsx("z-[101] text-right lg:basis-[50px]")}
/>
)}
<a
href={id}
className={clsx("absolute top-0 left-0 w-full h-full z-[100]")}
/>
</div>
)
}

View File

@@ -0,0 +1,144 @@
import React from "react"
import clsx from "clsx"
import { useThemeConfig, usePrismTheme } from "@docusaurus/theme-common"
import {
parseCodeBlockTitle,
parseLanguage,
parseLines,
containsLineNumbers,
useCodeWordWrap,
} from "@docusaurus/theme-common/internal"
import Highlight, { defaultProps, type Language } from "prism-react-renderer"
import Line from "@theme/CodeBlock/Line"
import Container from "@theme/CodeBlock/Container"
import type { Props } from "@theme/CodeBlock/Content/String"
import useIsBrowser from "@docusaurus/useIsBrowser"
import { ThemeConfig } from "@medusajs/docs"
import { CopyButton, Tooltip } from "docs-ui"
import { ExclamationCircleSolid, SquareTwoStackSolid } from "@medusajs/icons"
export default function CodeBlockString({
children,
className: blockClassName = "",
metastring,
title: titleProp,
showLineNumbers: showLineNumbersProp,
language: languageProp,
noReport = false,
noCopy = false,
}: Props): JSX.Element {
const {
prism: { defaultLanguage, magicComments },
reportCodeLinkPrefix = "",
} = useThemeConfig() as ThemeConfig
const language =
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage
const prismTheme = usePrismTheme()
const wordWrap = useCodeWordWrap()
const isBrowser = useIsBrowser()
// We still parse the metastring in case we want to support more syntax in the
// future. Note that MDX doesn't strip quotes when parsing metastring:
// "title=\"xyz\"" => title: "\"xyz\""
const title = parseCodeBlockTitle(metastring) || titleProp
const { lineClassNames, code } = parseLines(children, {
metastring,
language,
magicComments,
})
const showLineNumbers = showLineNumbersProp ?? containsLineNumbers(metastring)
return (
<Container
as="div"
className={clsx(
blockClassName,
language &&
!blockClassName.includes(`language-${language}`) &&
`language-${language}`
)}
>
{title && <div>{title}</div>}
<div className={clsx("relative rounded-[inherit]")}>
<Highlight
{...defaultProps}
theme={prismTheme}
code={code}
language={(language ?? "text") as Language}
>
{({ className, tokens, getLineProps, getTokenProps }) => (
<>
<pre
tabIndex={0}
ref={wordWrap.codeBlockRef}
className={clsx("m-0 p-0", "thin-scrollbar", className)}
>
<code
className={clsx(
"font-[inherit] float-left min-w-full print:whitespace-pre-wrap",
showLineNumbers &&
tokens.length > 1 &&
"table p-1 code-block-numbering",
title && "p-1",
!title && tokens.length > 1 && "p-1",
!title && tokens.length === 1 && "py-0.5 pr-0.5 pl-1"
)}
>
{tokens.map((line, i) => (
<Line
key={i}
line={line}
getLineProps={getLineProps}
getTokenProps={getTokenProps}
classNames={lineClassNames[i]}
showLineNumbers={showLineNumbers && tokens.length > 1}
/>
))}
</code>
</pre>
<div
className={clsx(
"flex gap-x-0.125 absolute right-1",
tokens.length === 1 && "top-0.25",
tokens.length > 1 && "top-1"
)}
>
{!noReport && (
<Tooltip text="Report Incorrect Code">
<a
href={`${reportCodeLinkPrefix}&title=${encodeURIComponent(
`Docs(Code Issue): Code Issue in ${
isBrowser ? location.pathname : ""
}`
)}`}
target="_blank"
className={clsx(
"bg-transparent border-none p-0.25 cursor-pointer rounded",
"hover:bg-medusa-code-bg-base dark:hover:bg-medusa-code-bg-base-dark [&:not(:first-child)]:ml-0.5",
"inline-flex justify-center items-center invisible xs:visible"
)}
rel="noreferrer"
>
<ExclamationCircleSolid className="text-medusa-code-icon dark:text-medusa-code-icon-dark" />
</a>
</Tooltip>
)}
{!noCopy && (
<CopyButton
buttonClassName={clsx(
"flex bg-transparent border-none p-0.25 cursor-pointer rounded"
)}
text={code}
>
<SquareTwoStackSolid className="text-medusa-code-icon dark:text-medusa-code-icon-dark" />
</CopyButton>
)}
</div>
</>
)}
</Highlight>
</div>
</Container>
)
}

View File

@@ -0,0 +1,59 @@
import React, { isValidElement, type ReactNode } from "react"
import useIsBrowser from "@docusaurus/useIsBrowser"
import ElementContent from "@theme/CodeBlock/Content/Element"
import StringContent from "@theme/CodeBlock/Content/String"
import type { Props } from "@theme/CodeBlock"
import clsx from "clsx"
/**
* Best attempt to make the children a plain string so it is copyable. If there
* are react elements, we will not be able to copy the content, and it will
* return `children` as-is; otherwise, it concatenates the string children
* together.
*/
function maybeStringifyChildren(children: ReactNode): ReactNode {
if (React.Children.toArray(children).some((el) => isValidElement(el))) {
return children
}
// The children is now guaranteed to be one/more plain strings
return Array.isArray(children) ? children.join("") : (children as string)
}
export default function CodeBlock({
children: rawChildren,
noReport = false,
noCopy = false,
...props
}: Props): JSX.Element {
// The Prism theme on SSR is always the default theme but the site theme can
// be in a different mode. React hydration doesn't update DOM styles that come
// from SSR. Hence force a re-render after mounting to apply the current
// relevant styles.
const isBrowser = useIsBrowser()
const children = maybeStringifyChildren(rawChildren)
const CodeBlockComp =
typeof children === "string" ? StringContent : ElementContent
const title = props.title
delete props.title
return (
<div className="code-wrapper">
{title && <div className="code-header">{title}</div>}
<CodeBlockComp
key={String(isBrowser)}
noReport={noReport}
noCopy={noCopy}
{...props}
className={clsx(
!title && "rounded",
title && "!rounded-t-none !rounded-b",
props.className
)}
showLineNumbers={true}
>
{children as string}
</CodeBlockComp>
</div>
)
}

View File

@@ -0,0 +1,26 @@
import React from "react"
import {
Details as UiDetails,
type DetailsProps as UiDetailsProps,
} from "docs-ui"
export type DetailsProps = {
summary: React.ReactNode
children?: React.ReactNode
} & UiDetailsProps
export default function Details({
summary,
children,
...props
}: DetailsProps): JSX.Element {
return (
<UiDetails
{...props}
summaryContent={!React.isValidElement(summary) ? summary : undefined}
summaryElm={React.isValidElement(summary) ? summary : undefined}
>
{children}
</UiDetails>
)
}

View File

@@ -0,0 +1,247 @@
import React, { type ReactNode } from "react"
import clsx from "clsx"
import Link from "@docusaurus/Link"
import {
findFirstCategoryLink,
useDocById,
} from "@docusaurus/theme-common/internal"
import isInternalUrl from "@docusaurus/isInternalUrl"
import { translate } from "@docusaurus/Translate"
import {
ModifiedDocCard,
ModifiedDocCardItemCategory,
ModifiedDocCardItemLink,
ModifiedSidebarItem,
} from "@medusajs/docs"
import { Badge } from "docs-ui"
import BorderedIcon from "../../components/BorderedIcon"
import Icons from "../Icon"
type ModifiedProps = {
item: ModifiedDocCard
}
function CardContainer({
href,
children,
className,
}: {
href: string
children: ReactNode
className?: string
}): JSX.Element {
return (
<article className={`card-wrapper margin-bottom--lg`}>
<Link
href={href}
className={clsx(
"card",
"bg-medusa-bg-subtle dark:bg-medusa-bg-base-dark",
"rounded shadow-card-rest dark:shadow-card-rest-dark",
"transition-all duration-200 ease-ease",
"flex p-1 !pb-1.5 h-full",
className
)}
>
{children}
</Link>
</article>
)
}
function CardLayout({
href,
icon,
title,
description,
html,
containerClassName,
isSoon = false,
badge,
}: ModifiedDocCard): JSX.Element {
const isHighlighted = containerClassName?.includes("card-highlighted")
return (
<CardContainer
href={href}
className={clsx(
containerClassName,
!isSoon &&
"hover:bg-medusa-bg-subtle-hover dark:hover:bg-medusa-bg-base-hover-dark",
isSoon && "pointer-events-none",
isHighlighted &&
"md:before:content-[''] md:before:absolute md:before:top-0 before:right-0 md:before:w-1/2 md:before:h-full md:before:bg-no-repeat md:before:bg-cover md:before:bg-card-highlighted dark:md:before:bg-card-highlighted-dark",
!isSoon && "hover:shadow-card-hover dark:hover:shadow-card-hover-dark"
)}
>
<div className={clsx("mb-1 flex justify-between items-center")}>
{icon}
{isSoon && <Badge variant={"purple"}>Guide coming soon</Badge>}
{badge && <Badge {...badge} />}
</div>
<div className={clsx("w-[calc(100%-20px)] [&>*:last-child]:mb-0")}>
<span
className={clsx(
"text-compact-medium-plus text-medusa-fg-base dark:text-medusa-fg-base-dark",
"mb-0.25 block",
"transition-all duration-200 ease-ease",
isSoon &&
"group-hover:text-medusa-fg-disabled dark:group-hover:text-medusa-fg-disabled-dark"
)}
title={title}
>
{title}
</span>
{description && (
<p
className={clsx(
"text-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"transition-all duration-200 ease-ease",
isSoon &&
"group-hover:text-medusa-fg-disabled dark:group-hover:text-medusa-fg-disabled-dark",
isHighlighted && "md:w-1/2"
)}
title={description}
>
{description}
</p>
)}
{html && (
<p
className={clsx(
"text-compact-medium text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
"transition-all duration-200 ease-ease",
isSoon &&
"group-hover:text-medusa-fg-disabled dark:group-hover:text-medusa-fg-disabled-dark",
isHighlighted && "md:w-1/2"
)}
dangerouslySetInnerHTML={{
__html: html,
}}
></p>
)}
</div>
</CardContainer>
)
}
function getCardIcon(item: ModifiedSidebarItem): JSX.Element {
if (item.customProps?.themedImage) {
return (
<BorderedIcon
icon={{
light: item.customProps.themedImage.light,
dark: item.customProps.themedImage.dark,
}}
iconWrapperClassName={clsx("p-[6px]")}
iconClassName={clsx("h-[20px] w-[20px]")}
/>
)
} else if (item.customProps?.image) {
return (
<BorderedIcon
icon={{
light: item.customProps.image,
}}
iconWrapperClassName={clsx("p-[6px]")}
iconClassName={clsx("h-[20px] w-[20px]")}
/>
)
} else if (item.customProps?.icon) {
return (
<BorderedIcon
IconComponent={item.customProps.icon}
iconWrapperClassName={clsx("p-[6px]")}
iconClassName={clsx("h-[20px] w-[20px]")}
/>
)
} else if (
item.customProps?.iconName &&
Object.hasOwn(Icons, item.customProps?.iconName)
) {
return (
<BorderedIcon
IconComponent={Icons[item.customProps?.iconName]}
iconWrapperClassName={clsx("p-[6px]")}
iconClassName={clsx("h-[20px] w-[20px]")}
/>
)
} else {
return (
<div
className={clsx(
// "card-icon-wrapper",
"no-zoom-img"
)}
>
{isInternalUrl(
"href" in item ? item.href : "value" in item ? item.value : "#"
)
? "📄️"
: "🔗"}
</div>
)
}
}
function CardCategory({
item,
}: {
item: ModifiedDocCardItemCategory
}): JSX.Element | null {
const href = findFirstCategoryLink(item)
const icon = getCardIcon(item)
// Unexpected: categories that don't have a link have been filtered upfront
if (!href) {
return null
}
return (
<CardLayout
{...item}
href={href}
icon={icon}
title={item.label}
// eslint-disable-next-line @docusaurus/string-literal-i18n-messages
description={translate(
{
message: item.customProps?.description || "{count} items",
id: "theme.docs.DocCard.categoryDescription",
description:
"The default description for a category card in the generated index about how many items this category includes",
},
{ count: item.items.length }
)}
containerClassName={item.customProps?.className}
isSoon={item.customProps?.isSoon}
badge={item.customProps?.badge}
/>
)
}
function CardLink({ item }: { item: ModifiedDocCardItemLink }): JSX.Element {
const icon = getCardIcon(item)
const doc = useDocById(item.docId ?? undefined)
return (
<CardLayout
{...item}
icon={icon}
title={item.label}
description={item.customProps?.description || doc?.description}
html={item.customProps?.html}
containerClassName={item.customProps?.className}
isSoon={item.customProps?.isSoon}
badge={item.customProps?.badge}
/>
)
}
export default function DocCard({ item }: ModifiedProps): JSX.Element {
switch (item.type) {
case "link":
return <CardLink item={item} />
case "category":
return <CardCategory item={item} />
default:
throw new Error(`unknown item type ${JSON.stringify(item)}`)
}
}

View File

@@ -0,0 +1,37 @@
import React from "react"
import clsx from "clsx"
import {
useCurrentSidebarCategory,
filterDocCardListItems,
} from "@docusaurus/theme-common"
import DocCard from "@theme/DocCard"
import type { Props } from "@theme/DocCardList"
function DocCardListForCurrentSidebarCategory({ className }: Props) {
const category = useCurrentSidebarCategory()
return <DocCardList items={category.items} className={className} />
}
type ModifiedProps = {
colSize?: string
} & Props
export default function DocCardList(props: ModifiedProps): JSX.Element {
const { items, className } = props
if (!items) {
return <DocCardListForCurrentSidebarCategory {...props} />
}
const filteredItems = filterDocCardListItems(items).filter(
(item) => !item.customProps.excludeFromDocList
)
return (
<section
className={clsx("cards-grid", `grid-${props.colSize || "4"}`, className)}
>
{filteredItems.map((item, index) => (
<DocCard item={item} key={index} />
))}
</section>
)
}

View File

@@ -0,0 +1,67 @@
import React from "react"
import clsx from "clsx"
import { ThemeClassNames } from "@docusaurus/theme-common"
import { useDoc } from "@docusaurus/theme-common/internal"
import Heading from "@theme/Heading"
import MDXContent from "@theme/MDXContent"
import type { Props } from "@theme/DocItem/Content"
import { DocContextValue } from "@medusajs/docs"
import { Badge, type BadgeVariant } from "docs-ui"
/**
Title can be declared inside md content or declared through
front matter and added manually. To make both cases consistent,
the added title is added under the same div.markdown block
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
We render a "synthetic title" if:
- user doesn't ask to hide it with front matter
- the markdown content does not already contain a top-level h1 heading
*/
function useSyntheticTitle(): string | null {
const { metadata, frontMatter, contentTitle } = useDoc()
const shouldRender =
!frontMatter.hide_title && typeof contentTitle === "undefined"
if (!shouldRender) {
return null
}
return metadata.title
}
export default function DocItemContent({ children }: Props): JSX.Element {
const {
frontMatter: { badge },
} = useDoc() as DocContextValue
const syntheticTitle = useSyntheticTitle()
return (
<div className={clsx(ThemeClassNames.docs.docMarkdown, "markdown")}>
{syntheticTitle && (
<header
className={clsx(badge && "md:flex md:items-center md:gap-0.5 mb-2")}
>
<Heading as="h1" className={clsx(badge && "!mb-0")}>
{syntheticTitle}
{badge && (
<Badge
variant={badge.variant as BadgeVariant}
className="md:hidden ml-1 align-middle"
>
{badge.text}
</Badge>
)}
</Heading>
{badge && (
<Badge
variant={badge.variant as BadgeVariant}
className={clsx("md:block hidden")}
>
{badge.text}
</Badge>
)}
</header>
)}
<MDXContent>{children}</MDXContent>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import React from "react"
import Footer from "@theme-original/DocItem/Footer"
import type FooterType from "@theme/DocItem/Footer"
import type { WrapperProps } from "@docusaurus/types"
import { useDoc } from "@docusaurus/theme-common/internal"
import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
import Feedback from "@site/src/components/Feedback"
type Props = WrapperProps<typeof FooterType>
export default function FooterWrapper(props: Props): JSX.Element {
const { metadata } = useDoc()
const { footerFeedback = { event: "" } } = useThemeConfig() as ThemeConfig
return (
<>
{!metadata.frontMatter?.hide_footer && (
<div className="mt-[42px]">
<Feedback
{...footerFeedback}
className="border-0 border-t border-solid border-medusa-border-base dark:border-medusa-border-base-dark"
/>
<Footer {...props} />
</div>
)}
</>
)
}

View File

@@ -0,0 +1,72 @@
import React from "react"
import clsx from "clsx"
import { useWindowSize } from "@docusaurus/theme-common"
import { useDoc } from "@docusaurus/theme-common/internal"
import DocItemPaginator from "@theme/DocItem/Paginator"
import DocVersionBanner from "@theme/DocVersionBanner"
import DocVersionBadge from "@theme/DocVersionBadge"
import DocItemFooter from "@theme/DocItem/Footer"
import DocItemTOCMobile from "@theme/DocItem/TOC/Mobile"
import DocItemTOCDesktop from "@theme/DocItem/TOC/Desktop"
import DocItemContent from "@theme/DocItem/Content"
import DocBreadcrumbs from "@theme/DocBreadcrumbs"
import type { Props } from "@theme/DocItem/Layout"
import Footer from "@theme/Footer"
import { useSidebar } from "../../../providers/Sidebar"
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const { frontMatter, toc } = useDoc()
const windowSize = useWindowSize()
const hidden = frontMatter.hide_table_of_contents
const canRender = !hidden && toc.length > 0
const mobile = canRender ? <DocItemTOCMobile /> : undefined
const desktop =
canRender && (windowSize === "desktop" || windowSize === "ssr") ? (
<DocItemTOCDesktop />
) : undefined
return {
hidden,
mobile,
desktop,
}
}
export default function DocItemLayout({ children }: Props): JSX.Element {
const docTOC = useDocTOC()
const sidebarContext = useSidebar()
return (
<div className="row m-0">
<div
className={clsx(
"col",
"my-0 mx-auto max-w-main-content w-full ml-auto lg:py-0 py-0 px-1",
!docTOC.hidden && "w-9/12",
!sidebarContext?.hiddenSidebarContainer && "!max-w-[720px]"
)}
>
<DocVersionBanner />
<div>
<article className={clsx("[&>*:first-child]:mt-0")}>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<DocItemFooter />
</article>
<DocItemPaginator />
<Footer />
</div>
</div>
{docTOC.desktop && (
<div className="col toc-wrapper">{docTOC.desktop}</div>
)}
</div>
)
}

View File

@@ -0,0 +1,31 @@
import React from "react"
import clsx from "clsx"
import { useDocsSidebar } from "@docusaurus/theme-common/internal"
import type { Props } from "@theme/DocPage/Layout/Main"
import { useSidebar } from "@site/src/providers/Sidebar"
export default function DocPageLayoutMain({ children }: Props): JSX.Element {
const sidebar = useDocsSidebar()
const sidebarContext = useSidebar()
return (
<main
className={clsx(
"flex flex-col w-full lg:flex-grow",
(sidebarContext?.hiddenSidebarContainer || !sidebar) &&
"lg:max-w-[calc(100%-30px)]",
!sidebarContext?.hiddenSidebarContainer &&
"xxl:max-w-[1119px] lg:max-w-[calc(100%-321px)]"
)}
>
<div
className={clsx(
"container padding-top--md px-0",
sidebarContext?.hiddenSidebarContainer &&
"lg:max-w-main-content-hidden-sidebar"
)}
>
{children}
</div>
</main>
)
}

View File

@@ -0,0 +1,105 @@
import React, { type ReactNode, useRef } from "react"
import clsx from "clsx"
import { ThemeClassNames } from "@docusaurus/theme-common"
import { useDocsSidebar } from "@docusaurus/theme-common/internal"
import { useLocation } from "@docusaurus/router"
import DocSidebar from "@theme/DocSidebar"
import type { Props } from "@theme/DocPage/Layout/Sidebar"
import { SwitchTransition, CSSTransition } from "react-transition-group"
import { useSidebar } from "@site/src/providers/Sidebar"
// Reset sidebar state when sidebar changes
// Use React key to unmount/remount the children
// See https://github.com/facebook/docusaurus/issues/3414
function ResetOnSidebarChange({ children }: { children: ReactNode }) {
const sidebar = useDocsSidebar()
return (
<React.Fragment key={sidebar?.name ?? "noSidebar"}>
{children}
</React.Fragment>
)
}
export default function DocPageLayoutSidebar({
sidebar,
hiddenSidebarContainer,
}: Props): JSX.Element {
const { pathname } = useLocation()
const sidebarContext = useSidebar()
const { name } = useDocsSidebar()
const sidebarRef = useRef(null)
return (
<aside
className={clsx(
ThemeClassNames.docs.docSidebarContainer,
"min-[997px]:block min-[997px]:w-sidebar min-[997px]:transition-[left] min-[997px]:ease-ease min-[997px]:duration-200 min-[997px]:left-0 hidden",
!hiddenSidebarContainer && "clip",
hiddenSidebarContainer &&
"min-[997px]:fixed min-[997px]:left-[-100%] min-[997px]:rounded min-[997px]:border-0 min-[997px]:border-medusa-border-strong min-[997px]:dark:border-medusa-border-strong-dark",
hiddenSidebarContainer &&
sidebarContext?.floatingSidebar &&
"min-[997px]:!left-0.5 min-[997px]:top-[65px] min-[997px]:z-20 min-[997px]:bg-docs-bg min-[997px]:dark:bg-docs-bg-dark min-[997px]:shadow-flyout min-[997px]:dark:shadow-flyout-dark"
)}
onTransitionEnd={(e) => {
if (
!e.currentTarget.classList.contains(
ThemeClassNames.docs.docSidebarContainer
)
) {
return
}
if (hiddenSidebarContainer) {
sidebarContext?.setHiddenSidebar(true)
}
}}
onMouseLeave={() => {
setTimeout(() => {
if (!document.querySelector(".sidebar-toggler:hover")) {
sidebarContext?.setFloatingSidebar(false)
}
}, 100)
}}
onMouseUp={(e) => {
const target = e.target as Element
if (
target.classList.contains("menu__list-item") ||
target.parentElement.classList.contains("menu__list-item")
) {
sidebarContext?.setFloatingSidebar(false)
}
}}
>
<SwitchTransition>
<CSSTransition
key={name}
nodeRef={sidebarRef}
classNames={{
enter: "animate__animated animate__fadeInLeft animate__fastest",
exit: "animate__animated animate__fadeOutLeft animate__fastest",
}}
timeout={200}
>
<div
className={clsx(
"min-[997px]:top-[57px] min-[997px]:sticky min-[997px]:max-h-screen min-[997px]:[&>div]:max-h-screen"
)}
ref={sidebarRef}
>
<ResetOnSidebarChange>
<div>
<DocSidebar
sidebar={sidebar}
path={pathname}
onCollapse={sidebarContext?.onCollapse}
isHidden={sidebarContext?.hiddenSidebar}
/>
</div>
</ResetOnSidebarChange>
</div>
</CSSTransition>
</SwitchTransition>
</aside>
)
}

View File

@@ -0,0 +1,40 @@
import React from "react"
import { useDocsSidebar } from "@docusaurus/theme-common/internal"
import Layout from "@theme/Layout"
import BackToTopButton from "@theme/BackToTopButton"
import DocPageLayoutSidebar from "@theme/DocPage/Layout/Sidebar"
import DocPageLayoutMain from "@theme/DocPage/Layout/Main"
import type { Props } from "@theme/DocPage/Layout"
import clsx from "clsx"
import { useSidebar } from "../../../providers/Sidebar"
import useOnboarding from "../../../hooks/use-onboarding"
import useCurrentLearningPath from "../../../hooks/use-current-learning-path"
export default function DocPageLayout({ children }: Props): JSX.Element {
const sidebar = useDocsSidebar()
const sidebarContext = useSidebar()
useOnboarding()
useCurrentLearningPath()
return (
<Layout wrapperClassName={clsx("flex flex-[1_0_auto]")}>
<BackToTopButton />
<div className={clsx("flex w-full flex-[1_0]")}>
{sidebar && (
<DocPageLayoutSidebar
sidebar={sidebar.items}
hiddenSidebarContainer={sidebarContext?.hiddenSidebarContainer}
setHiddenSidebarContainer={
sidebarContext?.setHiddenSidebarContainer
}
/>
)}
<DocPageLayoutMain
hiddenSidebarContainer={sidebarContext?.hiddenSidebarContainer}
>
{children}
</DocPageLayoutMain>
</div>
</Layout>
)
}

View File

@@ -0,0 +1,73 @@
import React from "react"
import clsx from "clsx"
import {
HtmlClassNameProvider,
ThemeClassNames,
PageMetadata,
} from "@docusaurus/theme-common"
import {
docVersionSearchTag,
DocsSidebarProvider,
DocsVersionProvider,
useDocRouteMetadata,
} from "@docusaurus/theme-common/internal"
import DocPageLayout from "@theme/DocPage/Layout"
import NotFound from "@theme/NotFound"
import SearchMetadata from "@theme/SearchMetadata"
import type { Props } from "@theme/DocPage"
import SidebarProvider from "@site/src/providers/Sidebar"
import DocsProviders from "../../providers/DocsProviders"
function DocPageMetadata(props: Props): JSX.Element {
const { versionMetadata } = props
return (
<>
<SearchMetadata
version={versionMetadata.version}
tag={docVersionSearchTag(
versionMetadata.pluginId,
versionMetadata.version
)}
/>
<PageMetadata>
{versionMetadata.noIndex && (
<meta name="robots" content="noindex, nofollow" />
)}
</PageMetadata>
</>
)
}
export default function DocPage(props: Props): JSX.Element {
const { versionMetadata } = props
const currentDocRouteMetadata = useDocRouteMetadata(props)
if (!currentDocRouteMetadata) {
return <NotFound />
}
const { docElement, sidebarName, sidebarItems } = currentDocRouteMetadata
return (
<>
<DocPageMetadata {...props} />
<HtmlClassNameProvider
className={clsx(
// TODO: it should be removed from here
ThemeClassNames.wrapper.docsPages,
ThemeClassNames.page.docsDocPage,
props.versionMetadata.className
// sidebarName && "doc-has-sidebar"
)}
>
<DocsVersionProvider version={versionMetadata}>
<DocsProviders>
<DocsSidebarProvider name={sidebarName} items={sidebarItems}>
<SidebarProvider sidebarName={sidebarName}>
<DocPageLayout>{docElement}</DocPageLayout>
</SidebarProvider>
</DocsSidebarProvider>
</DocsProviders>
</DocsVersionProvider>
</HtmlClassNameProvider>
</>
)
}

View File

@@ -0,0 +1,95 @@
import React, { useEffect, useRef } from "react"
import clsx from "clsx"
import { useThemeConfig } from "@docusaurus/theme-common"
import Content from "@theme/DocSidebar/Desktop/Content"
import type { Props } from "@theme/DocSidebar/Desktop"
import useIsBrowser from "@docusaurus/useIsBrowser"
import { useLocation } from "@docusaurus/router"
import AnnouncementBar from "../../AnnouncementBar/index"
function DocSidebarDesktop({ path, sidebar }: Props) {
const {
navbar: { hideOnScroll },
} = useThemeConfig()
const isBrowser = useIsBrowser()
const sidebarRef = useRef(null)
const location = useLocation()
useEffect(() => {
function handleScroll() {
if (!sidebarRef.current?.classList.contains("scrolling")) {
sidebarRef.current?.classList.add("scrolling")
const intervalId = setInterval(() => {
if (!sidebarRef.current?.matches(":hover")) {
sidebarRef.current?.classList.remove("scrolling")
clearInterval(intervalId)
}
}, 300)
}
}
if (isBrowser && sidebarRef.current) {
const navElement = sidebarRef.current.querySelector(".main-sidebar")
navElement.addEventListener("scroll", handleScroll)
return () => {
navElement?.removeEventListener("scroll", handleScroll)
}
}
}, [isBrowser, sidebarRef.current])
useEffect(() => {
const navElement = sidebarRef.current.querySelector(".main-sidebar")
const navElementBoundingRect = navElement.getBoundingClientRect()
// logic to scroll to current active item
const activeItem = document.querySelector(
".sidebar-desktop [aria-current=page]"
)
if (!activeItem) {
return
}
const activeItemBoundingReact = activeItem.getBoundingClientRect()
// the extra 160 is due to the sticky elements in the sidebar
const isActiveItemVisible =
activeItemBoundingReact.top >= 0 &&
activeItemBoundingReact.bottom + 160 <= navElementBoundingRect.height
if (!isActiveItemVisible) {
const elementToScrollTo = activeItem
const elementBounding = elementToScrollTo.getBoundingClientRect()
// scroll to element
navElement.scroll({
// the extra 160 is due to the sticky elements in the sidebar
top:
elementBounding.top -
navElementBoundingRect.top +
navElement.scrollTop -
160,
behaviour: "smooth",
})
}
}, [location])
return (
<div
className={clsx(
"lg:flex lg:flex-col lg:max-h-screen lg:h-full lg:sticky lg:top-0 lg:transition-opacity lg:duration-[50ms] lg:ease-ease lg:pt-1.5",
"sidebar-desktop",
hideOnScroll && "lg:pt-0"
)}
ref={sidebarRef}
>
<AnnouncementBar />
<Content
path={path}
sidebar={sidebar}
className={clsx("main-sidebar", "!mt-0 !pt-0 !px-1.5 !pb-4")}
/>
</div>
)
}
export default React.memo(DocSidebarDesktop)

View File

@@ -0,0 +1,238 @@
import React, { type ComponentProps, useEffect, useMemo } from "react"
import clsx from "clsx"
import {
ThemeClassNames,
useThemeConfig,
usePrevious,
Collapsible,
useCollapsible,
} from "@docusaurus/theme-common"
import {
isActiveSidebarItem,
findFirstCategoryLink,
useDocSidebarItemsExpandedState,
isSamePath,
} from "@docusaurus/theme-common/internal"
import Link from "@docusaurus/Link"
import { translate } from "@docusaurus/Translate"
import useIsBrowser from "@docusaurus/useIsBrowser"
import DocSidebarItems from "@theme/DocSidebarItems"
import type { Props } from "@theme/DocSidebarItem/Category"
import { ModifiedPropSidebarItemCategory } from "@medusajs/docs"
import DocSidebarItemIcon from "../../../components/DocSidebarItemIcon"
import { Badge } from "docs-ui"
type ModifiedProps = Props & {
item: ModifiedPropSidebarItemCategory
}
// If we navigate to a category and it becomes active, it should automatically
// expand itself
function useAutoExpandActiveCategory({
isActive,
collapsed,
updateCollapsed,
}: {
isActive: boolean
collapsed: boolean
updateCollapsed: (b: boolean) => void
}) {
const wasActive = usePrevious(isActive)
useEffect(() => {
const justBecameActive = isActive && !wasActive
if (justBecameActive && collapsed) {
updateCollapsed(false)
}
}, [isActive, wasActive, collapsed, updateCollapsed])
}
/**
* When a collapsible category has no link, we still link it to its first child
* during SSR as a temporary fallback. This allows to be able to navigate inside
* the category even when JS fails to load, is delayed or simply disabled
* React hydration becomes an optional progressive enhancement
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
* see https://github.com/facebook/docusaurus/issues/3030
*/
function useCategoryHrefWithSSRFallback(
item: Props["item"]
): string | undefined {
const isBrowser = useIsBrowser()
return useMemo(() => {
if (item.href) {
return item.href
}
// In these cases, it's not necessary to render a fallback
// We skip the "findFirstCategoryLink" computation
if (isBrowser || !item.collapsible) {
return undefined
}
return findFirstCategoryLink(item)
}, [item, isBrowser])
}
function CollapseButton({
categoryLabel,
onClick,
}: {
categoryLabel: string
onClick: ComponentProps<"button">["onClick"]
}) {
return (
<button
aria-label={translate(
{
id: "theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel",
message: "Toggle the collapsible sidebar category '{label}'",
description:
"The ARIA label to toggle the collapsible sidebar category",
},
{ label: categoryLabel }
)}
type="button"
className="hidden"
onClick={onClick}
/>
)
}
export default function DocSidebarItemCategory({
item,
onItemClick,
activePath,
level,
index,
...props
}: ModifiedProps): JSX.Element {
const { items, label, collapsible, className, href, customProps } = item
const {
docs: {
sidebar: { autoCollapseCategories },
},
} = useThemeConfig()
const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item)
const isActive = isActiveSidebarItem(item, activePath)
const isCurrentPage = isSamePath(href, activePath)
const { collapsed, setCollapsed } = useCollapsible({
// Active categories are always initialized as expanded. The default
// (`item.collapsed`) is only used for non-active categories.
initialState: () => {
if (!collapsible) {
return false
}
return isActive ? false : item.collapsed
},
})
const { expandedItem, setExpandedItem } = useDocSidebarItemsExpandedState()
// Use this instead of `setCollapsed`, because it is also reactive
const updateCollapsed = (toCollapsed = !collapsed) => {
setExpandedItem(toCollapsed ? null : index)
setCollapsed(toCollapsed)
}
useAutoExpandActiveCategory({ isActive, collapsed, updateCollapsed })
useEffect(() => {
if (
collapsible &&
expandedItem != null &&
expandedItem !== index &&
autoCollapseCategories
) {
setCollapsed(true)
}
}, [collapsible, expandedItem, index, setCollapsed, autoCollapseCategories])
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemCategory,
ThemeClassNames.docs.docSidebarItemCategoryLevel(level),
"menu__list-item",
// {
// "menu__list-item--collapsed": collapsed,
// },
className,
customProps?.sidebar_is_title && "sidebar-title",
customProps?.sidebar_is_group_headline && "sidebar-group-headline",
customProps?.sidebar_is_group_divider && "sidebar-group-divider",
customProps?.sidebar_is_divider_line && "sidebar-divider-line",
customProps?.sidebar_is_back_link && "sidebar-back-link",
customProps?.sidebar_is_soon &&
"sidebar-soon-link sidebar-badge-wrapper",
!customProps?.sidebar_is_title &&
"[&_.sidebar-item-icon]:w-[20px] [&_.sidebar-item-icon]:h-[20px]",
!customProps?.sidebar_is_title &&
!customProps?.sidebar_is_back_link &&
"[&_.sidebar-item-icon]:mr-0.75"
)}
>
<div
className={clsx("menu__list-item-collapsible", {
"menu__list-item-collapsible--active": isCurrentPage,
})}
>
<Link
className={clsx("menu__link", {
"menu__link--sublist": collapsible,
"menu__link--sublist-caret": !href && collapsible,
"menu__link--active": isActive,
})}
onClick={
collapsible
? (e) => {
onItemClick?.(item)
if (href) {
updateCollapsed(false)
} else {
e.preventDefault()
updateCollapsed()
}
}
: () => {
onItemClick?.(item)
}
}
aria-current={isCurrentPage ? "page" : undefined}
aria-expanded={collapsible ? !collapsed : undefined}
href={collapsible ? hrefWithSSRFallback ?? "#" : hrefWithSSRFallback}
{...props}
>
{customProps?.sidebar_icon && (
<DocSidebarItemIcon
icon={customProps.sidebar_icon}
is_title={customProps.sidebar_is_title}
is_disabled={customProps?.sidebar_is_soon}
/>
)}
{label}
</Link>
{href && collapsible && (
<CollapseButton
categoryLabel={label}
onClick={(e) => {
e.preventDefault()
updateCollapsed()
}}
/>
)}
{customProps?.sidebar_is_soon && (
<Badge variant="purple" className={`sidebar-soon-badge`}>
Soon
</Badge>
)}
</div>
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
<DocSidebarItems
items={items}
tabIndex={collapsed ? -1 : 0}
onItemClick={onItemClick}
activePath={activePath}
level={level + 1}
/>
</Collapsible>
</li>
)
}

View File

@@ -0,0 +1,60 @@
import React from "react"
import clsx from "clsx"
import { ThemeClassNames } from "@docusaurus/theme-common"
import type { Props } from "@theme/DocSidebarItem/Html"
import { ModifiedPropSidebarItemHtml } from "@medusajs/docs"
import DocSidebarItemIcon from "../../../components/DocSidebarItemIcon"
import { Badge } from "docs-ui"
type ModifiedProps = Props & {
item: ModifiedPropSidebarItemHtml
}
export default function DocSidebarItemHtml({
item,
level,
index,
}: ModifiedProps): JSX.Element {
const { value, defaultStyle, className, customProps } = item
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
defaultStyle && ["lg:py-[6px] lg:px-1", "menu__list-item"],
className,
customProps?.sidebar_is_title && "sidebar-title",
customProps?.sidebar_is_group_headline && "sidebar-group-headline",
customProps?.sidebar_is_group_divider && "sidebar-group-divider",
customProps?.sidebar_is_divider_line && "sidebar-divider-line",
customProps?.sidebar_is_back_link && "sidebar-back-link",
customProps?.sidebar_is_soon &&
"sidebar-soon-link sidebar-badge-wrapper",
!customProps?.sidebar_is_title &&
"[&_.sidebar-item-icon]:w-[20px] [&_.sidebar-item-icon]:h-[20px]",
!customProps?.sidebar_is_title &&
!customProps?.sidebar_is_back_link &&
"[&_.sidebar-item-icon]:mr-0.75"
)}
key={index}
>
{customProps?.sidebar_icon && (
<DocSidebarItemIcon
icon={customProps.sidebar_icon}
is_title={customProps.sidebar_is_title}
is_disabled={customProps?.sidebar_is_soon}
/>
)}
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: value }}
></span>
{customProps?.sidebar_is_soon && (
<Badge variant="purple" className={`sidebar-soon-badge`}>
Soon
</Badge>
)}
</li>
)
}

View File

@@ -0,0 +1,78 @@
import React from "react"
import clsx from "clsx"
import { ThemeClassNames } from "@docusaurus/theme-common"
import { isActiveSidebarItem } from "@docusaurus/theme-common/internal"
import Link from "@docusaurus/Link"
import isInternalUrl from "@docusaurus/isInternalUrl"
import IconExternalLink from "@theme/Icon/ExternalLink"
import type { Props } from "@theme/DocSidebarItem/Link"
import { ModifiedPropSidebarItemLink } from "@medusajs/docs"
import DocSidebarItemIcon from "../../../components/DocSidebarItemIcon"
import { Badge } from "docs-ui"
type ModifiedProps = Props & {
item: ModifiedPropSidebarItemLink
}
export default function DocSidebarItemLink({
item,
onItemClick,
activePath,
level,
...props
}: ModifiedProps): JSX.Element {
const { href, label, className, autoAddBaseUrl, customProps } = item
const isActive = isActiveSidebarItem(item, activePath)
const isInternalLink = isInternalUrl(href)
return (
<li
className={clsx(
ThemeClassNames.docs.docSidebarItemLink,
ThemeClassNames.docs.docSidebarItemLinkLevel(level),
"menu__list-item",
className,
customProps?.sidebar_is_title && "sidebar-title",
customProps?.sidebar_is_group_headline && "sidebar-group-headline",
customProps?.sidebar_is_group_divider && "sidebar-group-divider",
customProps?.sidebar_is_divider_line && "sidebar-divider-line",
customProps?.sidebar_is_back_link && "sidebar-back-link",
customProps?.sidebar_is_soon &&
"sidebar-soon-link sidebar-badge-wrapper",
!customProps?.sidebar_is_title &&
"[&_.sidebar-item-icon]:w-[20px] [&_.sidebar-item-icon]:h-[20px]",
!customProps?.sidebar_is_title &&
!customProps?.sidebar_is_back_link &&
"[&_.sidebar-item-icon]:mr-0.75"
)}
key={label}
>
<Link
className={clsx("menu__link", !isInternalLink && "items-center", {
"menu__link--active": isActive,
})}
autoAddBaseUrl={autoAddBaseUrl}
aria-current={isActive ? "page" : undefined}
to={href}
{...(isInternalLink && {
onClick: onItemClick ? () => onItemClick(item) : undefined,
})}
{...props}
>
{customProps?.sidebar_icon && (
<DocSidebarItemIcon
icon={customProps.sidebar_icon}
is_title={customProps.sidebar_is_title}
is_disabled={customProps?.sidebar_is_soon}
/>
)}
{label}
{!isInternalLink && <IconExternalLink />}
</Link>
{customProps?.sidebar_is_soon && (
<Badge variant="purple" className={`sidebar-soon-badge`}>
Soon
</Badge>
)}
</li>
)
}

View File

@@ -0,0 +1,26 @@
import React from "react"
import Translate from "@docusaurus/Translate"
import { ThemeClassNames } from "@docusaurus/theme-common"
import type { Props } from "@theme/EditThisPage"
import clsx from "clsx"
import { Button } from "docs-ui"
export default function EditThisPage({ editUrl }: Props): JSX.Element {
return (
<Button variant="secondary">
<a
href={editUrl}
target="_blank"
rel="noreferrer noopener"
className={clsx(ThemeClassNames.common.editThisPage)}
>
<Translate
id="theme.common.editThisPage"
description="The link label to edit the current page"
>
Edit this page
</Translate>
</a>
</Button>
)
}

View File

@@ -0,0 +1,51 @@
import React from "react"
import clsx from "clsx"
import type { Props } from "@theme/Footer/Layout"
import { useThemeConfig } from "@docusaurus/theme-common"
import { ThemeConfig } from "@medusajs/docs"
import SocialLinks from "@site/src/components/Footer/SocialLinks"
export default function FooterLayout({
style,
links,
logo,
copyright,
}: Props): JSX.Element {
const { socialLinks } = useThemeConfig() as ThemeConfig
return (
<footer
className={clsx(
"footer",
"border-t border-x-0 border-b-0 border-solid border-medusa-border-base dark:border-medusa-border-base-dark",
"pt-[108px] pb-4 mt-2",
{
"footer--dark": style === "dark",
}
)}
>
<div
className={clsx(
"container container-fluid",
"flex !px-0",
"[&_.col]:!px-0",
"lg:flex-row flex-col",
"!pt-0"
)}
>
{(logo || copyright || socialLinks) && (
<div className="col col--6">
<div className={clsx("lg:mb-0 mb-2")}>
{logo && <div>{logo}</div>}
{copyright}
</div>
</div>
)}
<div className={clsx("col col--6 row lg:justify-end justify-start")}>
{socialLinks && <SocialLinks links={socialLinks} />}
{links}
</div>
</div>
</footer>
)
}

View File

@@ -0,0 +1,26 @@
import React from "react"
import { useThemeConfig } from "@docusaurus/theme-common"
import FooterLinks from "@theme/Footer/Links"
import FooterLogo from "@theme/Footer/Logo"
import FooterCopyright from "@theme/Footer/Copyright"
import FooterLayout from "@theme/Footer/Layout"
function Footer(): JSX.Element | null {
const { footer } = useThemeConfig()
if (!footer) {
return null
}
const { copyright, links, logo, style } = footer
return (
<FooterLayout
style={style}
links={links && links.length > 0 && <FooterLinks links={links} />}
logo={logo && <FooterLogo logo={logo} />}
copyright={copyright && <FooterCopyright copyright={copyright} />}
/>
)
}
export default React.memo(Footer)

View File

@@ -0,0 +1,62 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconCircleDottedLine = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
d="M12.5 2.93589C10.884 2.3547 9.116 2.3547 7.5 2.93589"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2.63049 8.63198C2.94295 6.94471 3.82573 5.41606 5.13094 4.30209"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.13094 15.6979C3.82575 14.5839 2.94298 13.0552 2.63049 11.368"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.5 17.0641C9.116 17.6453 10.884 17.6453 12.5 17.0641"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M17.3695 8.63198C17.057 6.94471 16.1742 5.41606 14.869 4.30209"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.869 15.6979C16.1742 14.5839 17.057 13.0552 17.3695 11.368"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default IconCircleDottedLine

View File

@@ -0,0 +1,9 @@
import React from "react"
import { XMark } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
const IconClose = (props: IconProps) => {
return <XMark {...props} />
}
export default IconClose

View File

@@ -0,0 +1,9 @@
import React from "react"
import { SquareTwoStackSolid } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
const IconCopy = (props: IconProps) => {
return <SquareTwoStackSolid {...props} />
}
export default IconCopy

View File

@@ -0,0 +1,9 @@
import { Moon } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
const IconDarkMode = (props: IconProps) => {
return <Moon {...props} />
}
export default IconDarkMode

View File

@@ -0,0 +1,24 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconDiscord = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
d="M12.9953 12.4964C12.1086 12.4964 11.3775 11.6826 11.3775 10.6825C11.3775 9.68252 12.0938 8.86871 12.9953 8.86871C13.8968 8.86871 14.6268 9.6904 14.613 10.6825C14.5992 11.6747 13.9037 12.4964 12.9953 12.4964ZM7.01487 12.4964C6.12815 12.4964 5.3971 11.6826 5.3971 10.6825C5.3971 9.68252 6.11337 8.86871 7.01487 8.86871C7.91636 8.86871 8.64642 9.6904 8.63263 10.6825C8.61884 11.6747 7.91538 12.4964 7.01487 12.4964ZM16.2357 4.2765C15.069 3.741 13.8376 3.35915 12.5726 3.14052C12.5611 3.13824 12.5492 3.13968 12.5385 3.14464C12.5279 3.1496 12.5191 3.15782 12.5135 3.16811C12.3458 3.47256 12.1935 3.78528 12.0573 4.10507C10.6937 3.89797 9.3066 3.89797 7.94296 4.10507C7.80595 3.78447 7.65136 3.47168 7.4799 3.16811C7.47394 3.15812 7.46512 3.15016 7.45458 3.14524C7.44404 3.14032 7.43227 3.13868 7.42078 3.14052C6.15564 3.35869 4.92426 3.74056 3.75767 4.2765C3.74785 4.28081 3.73959 4.28803 3.73402 4.29719C1.40097 7.78297 0.761549 11.183 1.07387 14.5437C1.07476 14.552 1.07732 14.56 1.08138 14.5673C1.08545 14.5746 1.09093 14.5809 1.09752 14.5861C2.45592 15.5924 3.97543 16.3607 5.59119 16.858C5.60256 16.8614 5.61469 16.8613 5.62596 16.8576C5.63724 16.8539 5.64711 16.8468 5.65425 16.8373C6.00123 16.3649 6.30868 15.8647 6.57348 15.3417C6.57701 15.3346 6.57899 15.3267 6.57929 15.3187C6.5796 15.3107 6.57822 15.3028 6.57525 15.2953C6.57228 15.2879 6.56778 15.2812 6.56204 15.2756C6.55631 15.27 6.54946 15.2657 6.54195 15.2629C6.05702 15.0774 5.58757 14.8537 5.13798 14.5939C5.1293 14.5892 5.12196 14.5824 5.11666 14.5741C5.11135 14.5657 5.10826 14.5562 5.10766 14.5463C5.10707 14.5365 5.109 14.5266 5.11328 14.5177C5.11755 14.5088 5.12401 14.5011 5.13207 14.4954C5.2306 14.4245 5.32124 14.3516 5.4109 14.2767C5.41886 14.27 5.42854 14.2658 5.43883 14.2644C5.44911 14.263 5.45958 14.2645 5.46903 14.2688C8.41391 15.6137 11.6031 15.6137 14.5135 14.2688C14.523 14.2642 14.5336 14.2624 14.5441 14.2636C14.5546 14.2648 14.5645 14.269 14.5726 14.2757C14.6623 14.3496 14.7569 14.4245 14.8524 14.4944C14.8605 14.5001 14.8671 14.5077 14.8714 14.5165C14.8758 14.5254 14.8778 14.5352 14.8773 14.5451C14.8768 14.5549 14.8738 14.5645 14.8686 14.5729C14.8634 14.5812 14.8561 14.5882 14.8475 14.593C14.4006 14.8535 13.9327 15.0759 13.4485 15.258C13.4409 15.2608 13.4341 15.2653 13.4283 15.271C13.4226 15.2767 13.4182 15.2835 13.4153 15.291C13.4124 15.2986 13.4111 15.3066 13.4116 15.3147C13.4121 15.3228 13.4142 15.3306 13.4179 15.3378C13.6867 15.8581 13.9933 16.358 14.3352 16.8334C14.3421 16.8432 14.3519 16.8505 14.3632 16.8544C14.3745 16.8583 14.3868 16.8585 14.3983 16.8551C16.0168 16.3593 17.5388 15.591 18.8988 14.5831C18.9055 14.5783 18.911 14.5721 18.9151 14.565C18.9192 14.5578 18.9217 14.5499 18.9225 14.5417C19.2978 10.6599 18.2939 7.2874 16.2623 4.29522C16.2563 4.28616 16.2462 4.27953 16.2357 4.2765Z"
fill="currentColor"
/>
</svg>
)
}
export default IconDiscord

View File

@@ -0,0 +1,9 @@
import { ArrowUpRightOnBox } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
const IconExternalLink = (props: IconProps) => {
return <ArrowUpRightOnBox {...props} />
}
export default IconExternalLink

View File

@@ -0,0 +1,26 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconGitHub = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 1.22205C5.0275 1.22205 1 5.24955 1 10.222C1 14.2045 3.57625 17.5683 7.15375 18.7608C7.60375 18.8395 7.7725 18.5695 7.7725 18.3333C7.7725 18.1195 7.76125 17.4108 7.76125 16.657C5.5 17.0733 4.915 16.1058 4.735 15.5995C4.63375 15.3408 4.195 14.542 3.8125 14.3283C3.4975 14.1595 3.0475 13.7433 3.80125 13.732C4.51 13.7208 5.01625 14.3845 5.185 14.6545C5.995 16.0158 7.28875 15.6333 7.80625 15.397C7.885 14.812 8.12125 14.4183 8.38 14.1933C6.3775 13.9683 4.285 13.192 4.285 9.74955C4.285 8.7708 4.63375 7.9608 5.2075 7.3308C5.1175 7.1058 4.8025 6.1833 5.2975 4.9458C5.2975 4.9458 6.05125 4.70955 7.7725 5.8683C8.4925 5.6658 9.2575 5.56455 10.0225 5.56455C10.7875 5.56455 11.5525 5.6658 12.2725 5.8683C13.9938 4.6983 14.7475 4.9458 14.7475 4.9458C15.2425 6.1833 14.9275 7.1058 14.8375 7.3308C15.4113 7.9608 15.76 8.75955 15.76 9.74955C15.76 13.2033 13.6562 13.9683 11.6538 14.1933C11.98 14.4745 12.2613 15.0145 12.2613 15.8583C12.2613 17.062 12.25 18.0295 12.25 18.3333C12.25 18.5695 12.4188 18.8508 12.8688 18.7608C16.4238 17.5683 19 14.1933 19 10.222C19 5.24955 14.9725 1.22205 10 1.22205Z"
fill="currentColor"
/>
</svg>
)
}
export default IconGitHub

View File

@@ -0,0 +1,9 @@
import React from "react"
import { Sun } from "@medusajs/icons"
import { IconProps } from "@medusajs/icons/dist/types"
const IconLightMode = (props: IconProps) => {
return <Sun {...props} />
}
export default IconLightMode

View File

@@ -0,0 +1,24 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconLinkedIn = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
d="M15.25 1H4.75C2.67925 1 1 2.67925 1 4.75V15.25C1 17.3208 2.67925 19 4.75 19H15.25C17.3215 19 19 17.3208 19 15.25V4.75C19 2.67925 17.3215 1 15.25 1ZM7 15.25H4.75V7H7V15.25ZM5.875 6.049C5.1505 6.049 4.5625 5.4565 4.5625 4.726C4.5625 3.9955 5.1505 3.403 5.875 3.403C6.5995 3.403 7.1875 3.9955 7.1875 4.726C7.1875 5.4565 6.60025 6.049 5.875 6.049ZM16 15.25H13.75V11.047C13.75 8.521 10.75 8.71225 10.75 11.047V15.25H8.5V7H10.75V8.32375C11.797 6.38425 16 6.241 16 10.1808V15.25Z"
fill="currentColor"
/>
</svg>
)
}
export default IconLinkedIn

View File

@@ -0,0 +1,28 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconNextjs = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
d="M9.41129 1.01314C9.37262 1.01666 9.24959 1.02896 9.13885 1.03775C6.585 1.268 4.19285 2.64599 2.67777 4.76395C1.8341 5.94157 1.2945 7.27737 1.09062 8.69227C1.01855 9.18617 1.00977 9.33206 1.00977 10.0017C1.00977 10.6714 1.01855 10.8173 1.09062 11.3112C1.57924 14.6876 3.98194 17.5244 7.2406 18.5755C7.82414 18.7636 8.43931 18.8919 9.13885 18.9692C9.41129 18.9991 10.5889 18.9991 10.8613 18.9692C12.0688 18.8356 13.0918 18.5368 14.1007 18.0218C14.2553 17.9427 14.2852 17.9216 14.2641 17.9041C14.2501 17.8935 13.591 17.0094 12.8 15.9408L11.3623 13.9986L9.56069 11.3323C8.56938 9.86638 7.75383 8.66767 7.7468 8.66767C7.73977 8.66591 7.73274 9.85056 7.72923 11.2971C7.72395 13.8299 7.7222 13.9318 7.69056 13.9916C7.64486 14.0777 7.60971 14.1128 7.53589 14.1515C7.47964 14.1796 7.43043 14.1849 7.16502 14.1849H6.86095L6.7801 14.1339C6.72737 14.1005 6.6887 14.0566 6.66234 14.0056L6.62543 13.9265L6.62894 10.4025L6.63422 6.87663L6.6887 6.80808C6.71683 6.77117 6.77659 6.72372 6.81877 6.70087C6.89083 6.66571 6.91895 6.6622 7.22303 6.6622C7.58158 6.6622 7.64134 6.67626 7.7345 6.7782C7.76086 6.80632 8.73635 8.27571 9.90343 10.0457C11.0705 11.8156 12.6664 14.2324 13.4503 15.4188L14.874 17.5754L14.9461 17.5279C15.5841 17.1131 16.2591 16.5226 16.7934 15.9074C17.9306 14.6015 18.6635 13.009 18.9096 11.3112C18.9816 10.8173 18.9904 10.6714 18.9904 10.0017C18.9904 9.33206 18.9816 9.18617 18.9096 8.69227C18.421 5.31585 16.0183 2.47901 12.7596 1.42794C12.1848 1.24163 11.5732 1.11333 10.8877 1.03599C10.719 1.01841 9.55717 0.999079 9.41129 1.01314ZM13.0918 6.45128C13.1762 6.49346 13.2447 6.57432 13.2693 6.65868C13.2834 6.70438 13.2869 7.68163 13.2834 9.88395L13.2781 13.0442L12.7209 12.19L12.162 11.3358V9.03853C12.162 7.55332 12.169 6.71844 12.1796 6.67802C12.2077 6.57959 12.2692 6.50225 12.3536 6.45655C12.4256 6.41964 12.452 6.41613 12.728 6.41613C12.9881 6.41613 13.0338 6.41964 13.0918 6.45128Z"
fill="currentColor"
/>
<path
d="M14.7861 17.6141C14.7246 17.6527 14.7053 17.6791 14.7598 17.6492C14.7984 17.6264 14.8617 17.5789 14.8512 17.5771C14.8459 17.5771 14.816 17.5947 14.7861 17.6141ZM14.6648 17.6932C14.6332 17.7178 14.6332 17.7195 14.6719 17.7002C14.693 17.6896 14.7105 17.6773 14.7105 17.6738C14.7105 17.6598 14.7018 17.6633 14.6648 17.6932ZM14.577 17.7459C14.5453 17.7705 14.5453 17.7722 14.584 17.7529C14.6051 17.7424 14.6227 17.7301 14.6227 17.7265C14.6227 17.7125 14.6139 17.716 14.577 17.7459ZM14.4891 17.7986C14.4574 17.8232 14.4574 17.825 14.4961 17.8056C14.5172 17.7951 14.5348 17.7828 14.5348 17.7793C14.5348 17.7652 14.526 17.7687 14.4891 17.7986ZM14.3555 17.8689C14.2887 17.9041 14.2922 17.9181 14.359 17.8847C14.3889 17.8689 14.4117 17.8531 14.4117 17.8496C14.4117 17.8373 14.41 17.839 14.3555 17.8689Z"
fill="currentColor"
/>
</svg>
)
}
export default IconNextjs

View File

@@ -0,0 +1,24 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconPuzzleSolid = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
d="M9.375 4.4475C9.375 4.15167 9.22 3.88417 9.04083 3.64833C8.85426 3.40948 8.752 3.11558 8.75 2.8125C8.75 1.94917 9.58917 1.25 10.625 1.25C11.6608 1.25 12.5 1.95 12.5 2.8125C12.5 3.12 12.3933 3.40667 12.2092 3.64833C12.03 3.88417 11.875 4.15167 11.875 4.4475C11.875 4.72417 12.1067 4.94583 12.3833 4.92917C13.975 4.83417 15.5417 4.64417 17.0767 4.36583C17.1603 4.35068 17.2461 4.35273 17.3289 4.37186C17.4117 4.39099 17.4897 4.42681 17.5582 4.47712C17.6267 4.52742 17.6842 4.59117 17.7272 4.66445C17.7702 4.73773 17.7978 4.81902 17.8083 4.90333C17.9969 6.4108 18.1015 7.92758 18.1217 9.44667C18.1224 9.51927 18.1088 9.59129 18.0814 9.65856C18.0541 9.72582 18.0137 9.78698 17.9625 9.83848C17.9113 9.88997 17.8504 9.93077 17.7833 9.9585C17.7162 9.98623 17.6443 10.0003 17.5717 10C17.2767 10 17.0092 9.845 16.7733 9.66583C16.5345 9.47926 16.2406 9.377 15.9375 9.375C15.075 9.375 14.375 10.2142 14.375 11.25C14.375 12.2858 15.075 13.125 15.9375 13.125C16.245 13.125 16.5317 13.0183 16.7733 12.8342C17.0092 12.655 17.2767 12.5 17.5725 12.5C17.8308 12.5 18.0367 12.7183 18.0175 12.9758C17.9197 14.3291 17.7542 15.6766 17.5217 17.0133C17.4996 17.1401 17.4389 17.2569 17.3479 17.3479C17.2569 17.4389 17.1401 17.4996 17.0133 17.5217C15.4967 17.7858 13.9525 17.9658 12.3842 18.0567C12.3185 18.0602 12.2528 18.0503 12.1911 18.0275C12.1294 18.0047 12.0729 17.9696 12.0253 17.9243C11.9776 17.879 11.9397 17.8244 11.9139 17.7639C11.888 17.7034 11.8748 17.6383 11.875 17.5725C11.875 17.2767 12.03 17.0092 12.2092 16.7733C12.3933 16.5317 12.5 16.245 12.5 15.9375C12.5 15.075 11.6608 14.375 10.625 14.375C9.58917 14.375 8.75 15.075 8.75 15.9375C8.75 16.245 8.85667 16.5317 9.04083 16.7733C9.22 17.0092 9.375 17.2767 9.375 17.5725C9.37525 17.644 9.36114 17.7148 9.33351 17.7808C9.30589 17.8467 9.2653 17.9064 9.21417 17.9564C9.16303 18.0064 9.10239 18.0456 9.03583 18.0717C8.96926 18.0978 8.89814 18.1102 8.82667 18.1083C7.51484 18.0713 6.20555 17.9712 4.90333 17.8083C4.81902 17.7978 4.73773 17.7702 4.66445 17.7272C4.59117 17.6842 4.52742 17.6267 4.47712 17.5582C4.42681 17.4897 4.39099 17.4117 4.37186 17.3289C4.35273 17.2461 4.35068 17.1603 4.36583 17.0767C4.61 15.7317 4.78583 14.3625 4.89 12.9733C4.89436 12.9127 4.88614 12.8517 4.86586 12.7944C4.84557 12.737 4.81365 12.6845 4.77211 12.64C4.73056 12.5956 4.68028 12.5602 4.62443 12.5361C4.56857 12.512 4.50833 12.4997 4.4475 12.5C4.15167 12.5 3.88417 12.655 3.64833 12.8342C3.40667 13.0183 3.12 13.125 2.8125 13.125C1.94917 13.125 1.25 12.2858 1.25 11.25C1.25 10.2142 1.95 9.375 2.8125 9.375C3.12 9.375 3.40667 9.48167 3.64833 9.66583C3.88417 9.845 4.15167 10 4.4475 10C4.51999 10.0003 4.59183 9.98625 4.65883 9.95856C4.72582 9.93087 4.78665 9.89013 4.83775 9.83872C4.88886 9.7873 4.92922 9.72624 4.95651 9.65907C4.98379 9.59191 4.99744 9.51999 4.99667 9.4475C4.97844 8.10511 4.89221 6.7645 4.73833 5.43083C4.72743 5.33729 4.7378 5.2425 4.76868 5.15353C4.79956 5.06457 4.85015 4.98373 4.91666 4.91706C4.98317 4.85039 5.06388 4.79961 5.15277 4.76852C5.24167 4.73742 5.33643 4.72682 5.43 4.7375C6.54917 4.86667 7.6825 4.94917 8.8275 4.9825C8.89886 4.9844 8.96988 4.97195 9.03634 4.94589C9.1028 4.91983 9.16336 4.8807 9.21441 4.83081C9.26547 4.78092 9.30599 4.72128 9.33358 4.65544C9.36116 4.5896 9.37525 4.51889 9.375 4.4475Z"
fill="currentColor"
/>
</svg>
)
}
export default IconPuzzleSolid

View File

@@ -0,0 +1,24 @@
import { IconProps } from "@medusajs/icons/dist/types"
import clsx from "clsx"
import React from "react"
const IconTwitter = (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}
className={clsx("text-ui-fg-subtle", props.className)}
>
<path
d="M18.5921 4.33226C18.3093 4.45768 18.0186 4.56353 17.7215 4.64947C18.0732 4.25173 18.3413 3.78373 18.505 3.2716C18.5417 3.15681 18.5037 3.03116 18.4093 2.95608C18.3151 2.88094 18.1841 2.87194 18.0804 2.93344C17.4495 3.3076 16.7689 3.5765 16.0552 3.73374C15.3363 3.03127 14.3599 2.6315 13.3505 2.6315C11.2199 2.6315 9.4864 4.3649 9.4864 6.49551C9.4864 6.66332 9.49703 6.83019 9.51805 6.99475C6.87409 6.76261 4.41605 5.46307 2.72812 3.39255C2.66797 3.31875 2.5753 3.27898 2.48042 3.28658C2.38549 3.29402 2.30019 3.34755 2.25223 3.42983C1.90988 4.01726 1.72889 4.68913 1.72889 5.37272C1.72889 6.30378 2.06131 7.18717 2.64852 7.87744C2.46997 7.81561 2.2967 7.73832 2.13134 7.64652C2.04256 7.59711 1.93421 7.59786 1.84601 7.64844C1.75775 7.69901 1.70242 7.79203 1.70009 7.8937C1.69969 7.91083 1.69969 7.92796 1.69969 7.94532C1.69969 9.33509 2.44767 10.5863 3.59125 11.2683C3.493 11.2585 3.39482 11.2442 3.29727 11.2256C3.1967 11.2064 3.09329 11.2416 3.02547 11.3183C2.95754 11.395 2.93506 11.5018 2.96636 11.5994C3.38965 12.9209 4.47946 13.893 5.79694 14.1893C4.70423 14.8737 3.45462 15.2322 2.14283 15.2322C1.86912 15.2322 1.59384 15.2161 1.32442 15.1843C1.19058 15.1684 1.06261 15.2474 1.01703 15.3747C0.971445 15.502 1.01975 15.644 1.13362 15.7169C2.81882 16.7975 4.7674 17.3686 6.76859 17.3686C10.7027 17.3686 13.1637 15.5134 14.5355 13.9571C16.2461 12.0166 17.2271 9.44797 17.2271 6.91003C17.2271 6.80401 17.2255 6.69694 17.2223 6.59021C17.8971 6.08174 18.4782 5.46638 18.951 4.7591C19.0228 4.65168 19.0151 4.50971 18.9318 4.41083C18.8488 4.31188 18.7103 4.27989 18.5921 4.33226Z"
fill="currentColor"
/>
</svg>
)
}
export default IconTwitter

View File

@@ -0,0 +1,210 @@
import {
AcademicCapSolid,
Adjustments,
ArrowDownLeftMini,
ArrowDownTray,
ArrowUpRightOnBox,
ArrowUturnLeft,
BarsThree,
BellAlert,
BellAlertSolid,
Bolt,
BoltSolid,
BookOpen,
Bug,
BugAntSolid,
BuildingStorefront,
BuildingTax,
BuildingsSolid,
Calendar,
CashSolid,
Channels,
ChannelsSolid,
CheckCircleSolid,
CheckMini,
ChevronDoubleLeftMiniSolid,
ChevronUpDown,
CircleDottedLine,
CircleMiniSolid,
CircleStack,
CircleStackSolid,
ClockSolidMini,
CloudArrowUp,
CogSixTooth,
CogSixToothSolid,
CommandLine,
CommandLineSolid,
ComponentSolid,
ComputerDesktop,
ComputerDesktopSolid,
CreditCardSolid,
CubeSolid,
CurrencyDollar,
CurrencyDollarSolid,
Discord,
DocumentText,
DocumentTextSolid,
EllipseMiniSolid,
ExclamationCircle,
ExclamationCircleSolid,
FlyingBox,
Folder,
FolderOpen,
Gatsby,
GiftSolid,
Github,
GlobeEurope,
GlobeEuropeSolid,
InformationCircleSolid,
JavascriptEx,
Key,
KeySolid,
LightBulb,
LightBulbSolid,
Linkedin,
MagnifyingGlass,
Map,
Moon,
Newspaper,
PencilSquareSolid,
Puzzle,
ReactJsEx,
ReceiptPercent,
RocketLaunch,
Server,
ServerSolid,
ServerStack,
ServerStackSolid,
ShoppingCart,
ShoppingCartSolid,
Sidebar,
Sparkles,
SparklesSolid,
SquareTwoStackSolid,
SquaresPlus,
SquaresPlusSolid,
Star,
StarSolid,
Stripe,
Sun,
SwatchSolid,
TagSolid,
Tools,
ToolsSolid,
Twitter,
User,
UsersSolid,
XCircleSolid,
XMark,
XMarkMini,
PhotoSolid,
} from "@medusajs/icons"
import IconPuzzleSolid from "./PuzzleSolid"
import IconNextjs from "./Nextjs"
export default {
"academic-cap-solid": AcademicCapSolid,
adjustments: Adjustments,
alert: ExclamationCircleSolid,
"arrow-down-left-mini": ArrowDownLeftMini,
"arrow-down-tray": ArrowDownTray,
"back-arrow": ArrowUturnLeft,
"bars-three": BarsThree,
bell: BellAlert,
"bell-alert-solid": BellAlertSolid,
bolt: Bolt,
"bolt-solid": BoltSolid,
"book-open": BookOpen,
bug: Bug,
"bug-ant-solid": BugAntSolid,
"building-solid": BuildingsSolid,
"building-storefront": BuildingStorefront,
"building-tax": BuildingTax,
calendar: Calendar,
"cash-solid": CashSolid,
"channels-solid": ChannelsSolid,
channels: Channels,
"check-circle-solid": CheckCircleSolid,
"check-mini": CheckMini,
"chevron-double-left-mini-solid": ChevronDoubleLeftMiniSolid,
"chevron-up-down": ChevronUpDown,
"circle-dotted-line": CircleDottedLine,
"circle-mini-solid": CircleMiniSolid,
"circle-stack": CircleStack,
"circle-stack-solid": CircleStackSolid,
"clock-solid-mini": ClockSolidMini,
close: XMark,
"cloud-arrow-up": CloudArrowUp,
"cog-six-tooth": CogSixTooth,
"cog-six-tooth-solid": CogSixToothSolid,
"command-line": CommandLine,
"command-line-solid": CommandLineSolid,
"component-solid": ComponentSolid,
"computer-desktop": ComputerDesktop,
"computer-desktop-solid": ComputerDesktopSolid,
copy: SquareTwoStackSolid,
"credit-card-solid": CreditCardSolid,
"cube-solid": CubeSolid,
"currency-dollar": CurrencyDollar,
"currency-dollar-solid": CurrencyDollarSolid,
"dark-mode": Moon,
discord: Discord,
"document-text": DocumentText,
"document-text-solid": DocumentTextSolid,
"ellipse-mini-solid": EllipseMiniSolid,
"exclamation-circle-solid": ExclamationCircleSolid,
"external-link": ArrowUpRightOnBox,
"flying-box": FlyingBox,
folder: Folder,
"folder-open": FolderOpen,
gatsby: Gatsby,
"gift-solid": GiftSolid,
github: Github,
"globe-europe": GlobeEurope,
"globe-europe-solid": GlobeEuropeSolid,
"information-circle-solid": InformationCircleSolid,
javascript: JavascriptEx,
key: Key,
"key-solid": KeySolid,
"light-bulb": LightBulb,
"light-bulb-solid": LightBulbSolid,
"light-mode": Sun,
linkedin: Linkedin,
"magnifying-glass": MagnifyingGlass,
map: Map,
newspaper: Newspaper,
nextjs: IconNextjs,
"pencil-square-solid": PencilSquareSolid,
"photo-solid": PhotoSolid,
puzzle: Puzzle,
// TODO change once available in Icons package
"puzzle-solid": IconPuzzleSolid,
react: ReactJsEx,
"receipt-percent": ReceiptPercent,
report: ExclamationCircle,
"rocket-launch": RocketLaunch,
server: Server,
"server-solid": ServerSolid,
"server-stack": ServerStack,
"server-stack-solid": ServerStackSolid,
"shopping-cart": ShoppingCart,
"shopping-cart-solid": ShoppingCartSolid,
sidebar: Sidebar,
sparkles: Sparkles,
"sparkles-solid": SparklesSolid,
"squares-plus": SquaresPlus,
"squares-plus-solid": SquaresPlusSolid,
star: Star,
"star-solid": StarSolid,
stripe: Stripe,
"swatch-solid": SwatchSolid,
"tag-solid": TagSolid,
tools: Tools,
"tools-solid": ToolsSolid,
twitter: Twitter,
user: User,
"users-solid": UsersSolid,
"x-circle-solid": XCircleSolid,
"x-mark": XMark,
"x-mark-mini": XMarkMini,
}

View File

@@ -0,0 +1,74 @@
import React, { useEffect } from "react"
import clsx from "clsx"
import ErrorBoundary from "@docusaurus/ErrorBoundary"
import {
PageMetadata,
SkipToContentFallbackId,
ThemeClassNames,
} from "@docusaurus/theme-common"
import { useKeyboardNavigation } from "@docusaurus/theme-common/internal"
import SkipToContent from "@theme/SkipToContent"
import Navbar from "@theme/Navbar"
import LayoutProvider from "@theme/Layout/Provider"
import ErrorPageContent from "@theme/ErrorPageContent"
import type { Props } from "@theme/Layout"
import useIsBrowser from "@docusaurus/useIsBrowser"
import { useLocation } from "@docusaurus/router"
import { useAnalytics } from "docs-ui"
export default function Layout(props: Props): JSX.Element {
const {
children,
wrapperClassName,
// Not really layout-related, but kept for convenience/retro-compatibility
title,
description,
} = props
useKeyboardNavigation()
const isBrowser = useIsBrowser()
const location = useLocation()
const { track } = useAnalytics()
useEffect(() => {
if (isBrowser) {
const handlePlay = () => {
track("video_played")
}
const videos = document.querySelectorAll("video")
videos.forEach((video) =>
video.addEventListener("play", handlePlay, {
once: true,
capture: true,
})
)
return () => {
videos.forEach((video) => video.removeEventListener("play", handlePlay))
}
}
}, [isBrowser, location.pathname])
return (
<LayoutProvider>
<PageMetadata title={title} description={description} />
<SkipToContent />
<Navbar />
<div
id={SkipToContentFallbackId}
className={clsx(
ThemeClassNames.wrapper.main,
"flex-auto flex-grow flex-shrink-0",
wrapperClassName
)}
>
<ErrorBoundary fallback={(params) => <ErrorPageContent {...params} />}>
{children}
</ErrorBoundary>
</div>
</LayoutProvider>
)
}

View File

@@ -0,0 +1,48 @@
import React, { useMemo } from "react"
import type { Props } from "@theme/MDXComponents/A"
import { getGlossaryByPath } from "../../utils/glossary"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import { MedusaDocusaurusContext } from "@medusajs/docs"
import Link from "@docusaurus/Link"
import clsx from "clsx"
import { Tooltip } from "docs-ui"
const MDXA = (props: Props) => {
const { href, children } = props
const {
siteConfig: { url },
} = useDocusaurusContext() as MedusaDocusaurusContext
// check if a glossary exists for href
const glossary = useMemo(() => {
return (typeof children === "string" && href.startsWith("/")) ||
href.includes(url)
? getGlossaryByPath(children as string)
: null
}, [href, children])
if (!glossary) {
return <Link {...props} />
}
return (
<Tooltip
tooltipChildren={
<span className="flex flex-col gap-0.25 max-w-[200px]">
<span className="text-compact-small-plus text-medusa-fg-base dark:text-medusa-fg-base-dark">
{glossary.title}
</span>
<span className="text-compact-small">{glossary.content}</span>
</span>
}
clickable={true}
>
<Link
{...props}
className={clsx(props.className, "border-0 border-b border-dashed")}
/>
</Tooltip>
)
}
export default MDXA

View File

@@ -0,0 +1,24 @@
import React, { type ComponentProps, type ReactElement } from "react"
import type { Props } from "@theme/MDXComponents/Details"
import Details, { DetailsProps } from "../Details"
import { type DetailsSummaryProps } from "docs-ui"
type MDXDetailsProps = Props & DetailsProps
export default function MDXDetails(props: MDXDetailsProps): JSX.Element {
const items = React.Children.toArray(props.children)
// Split summary item from the rest to pass it as a separate prop to the
// Details theme component
const summary = items.find(
(item): item is ReactElement<ComponentProps<"summary">> =>
React.isValidElement(item) &&
(item.props as { mdxType: string } | null)?.mdxType === "summary"
) as React.ReactElement<DetailsSummaryProps>
const children = <>{items.filter((item) => item !== summary)}</>
return (
<Details summary={summary} {...props}>
{children}
</Details>
)
}

View File

@@ -0,0 +1,17 @@
// Import the original mapper
import MDXComponents from "@theme-original/MDXComponents"
import CloudinaryImage from "@site/src/components/CloudinaryImage"
import MDXDetails from "./Details"
import MDXA from "./A"
import { Kbd, DetailsSummary, InlineCode } from "docs-ui"
export default {
// Re-use the default mapping
...MDXComponents,
inlineCode: InlineCode,
img: CloudinaryImage,
details: MDXDetails,
summary: DetailsSummary,
a: MDXA,
kbd: Kbd,
}

View File

@@ -0,0 +1,79 @@
import React from "react"
import Link from "@docusaurus/Link"
import useBaseUrl from "@docusaurus/useBaseUrl"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import { useThemeConfig, type NavbarLogo } from "@docusaurus/theme-common"
import ThemedImage from "@theme/ThemedImage"
import type { Props } from "@theme/Logo"
import { ThemeConfig } from "@medusajs/docs"
function LogoThemedImage({
logo,
alt,
imageClassName,
}: {
logo: NavbarLogo
alt: string
imageClassName?: string
}) {
const sources = {
light: useBaseUrl(logo.src),
dark: useBaseUrl(logo.srcDark || logo.src),
}
const themedImage = (
<ThemedImage
className={logo.className}
sources={sources}
height={logo.height}
width={logo.width}
alt={alt}
style={logo.style}
/>
)
// Is this extra div really necessary?
// introduced in https://github.com/facebook/docusaurus/pull/5666
return imageClassName ? (
<div className={imageClassName}>{themedImage}</div>
) : (
themedImage
)
}
export default function MobileLogo(props: Props): JSX.Element {
const {
siteConfig: { title },
} = useDocusaurusContext()
const {
navbar: { title: navbarTitle },
mobileLogo: logo,
} = useThemeConfig() as ThemeConfig
const { imageClassName, titleClassName, ...propsRest } = props
const logoLink = useBaseUrl(logo?.href || "/")
// If visible title is shown, fallback alt text should be
// an empty string to mark the logo as decorative.
const fallbackAlt = navbarTitle ? "" : title
// Use logo alt text if provided (including empty string),
// and provide a sensible fallback otherwise.
const alt = logo?.alt ?? fallbackAlt
return (
<Link
to={logoLink}
{...propsRest}
{...(logo?.target && { target: logo.target })}
>
{logo && (
<LogoThemedImage
logo={logo}
alt={alt}
imageClassName={imageClassName}
/>
)}
{navbarTitle != null && <b className={titleClassName}>{navbarTitle}</b>}
</Link>
)
}

View File

@@ -0,0 +1,27 @@
import React from "react"
import { useColorMode, useThemeConfig } from "@docusaurus/theme-common"
import ColorModeToggle from "@theme/ColorModeToggle"
import type { Props } from "@theme/Navbar/ColorModeToggle"
import clsx from "clsx"
export default function NavbarColorModeToggle({
className,
}: Props): JSX.Element | null {
const disabled = useThemeConfig().colorMode.disableSwitch
const { colorMode, setColorMode } = useColorMode()
if (disabled) {
return null
}
return (
<ColorModeToggle
className={clsx("text-ui-fg-muted", className)}
buttonClassName={clsx(
"hover:!bg-medusa-button-neutral-hover dark:hover:!bg-medusa-button-neutral-hover-dark"
)}
value={colorMode}
onChange={setColorMode}
/>
)
}

View File

@@ -0,0 +1,167 @@
import React, { type ReactNode } from "react"
import { useThemeConfig } from "@docusaurus/theme-common"
import {
splitNavbarItems,
useNavbarMobileSidebar,
} from "@docusaurus/theme-common/internal"
import NavbarItem, { type Props as NavbarItemConfig } from "@theme/NavbarItem"
import NavbarColorModeToggle from "@theme/Navbar/ColorModeToggle"
import NavbarMobileSidebarToggle from "@theme/Navbar/MobileSidebar/Toggle"
import NavbarLogo from "@theme/Navbar/Logo"
import NavbarActions from "../../../components/Navbar/Actions"
import { ThemeConfig } from "@medusajs/docs"
import useIsBrowser from "@docusaurus/useIsBrowser"
import clsx from "clsx"
import { useSidebar } from "../../../providers/Sidebar"
import { Tooltip } from "docs-ui"
import { ChevronDoubleLeftMiniSolid, Sidebar } from "@medusajs/icons"
function useNavbarItems() {
// TODO temporary casting until ThemeConfig type is improved
return useThemeConfig().navbar.items as NavbarItemConfig[]
}
function NavbarItems({ items }: { items: NavbarItemConfig[] }): JSX.Element {
return (
<>
{items.map((item, i) => (
<NavbarItem {...item} key={i} />
))}
</>
)
}
function NavbarContentLayout({
left,
right,
}: {
left: ReactNode
right: ReactNode
}) {
return (
<div
className={clsx(
"flex flex-wrap justify-between items-center w-full",
"lg:max-w-xl mx-auto py-0.5 px-1"
)}
>
<div className={clsx("items-center flex flex-1 min-w-0 gap-1.5")}>
{left}
</div>
<div
className={clsx("items-center flex lg:flex-1 min-w-0", "justify-end")}
>
{right}
</div>
</div>
)
}
export default function NavbarContent(): JSX.Element {
const mobileSidebar = useNavbarMobileSidebar()
const items = useNavbarItems()
const [leftItems, rightItems] = splitNavbarItems(items)
const {
navbarActions,
docs: {
sidebar: { hideable },
},
} = useThemeConfig() as ThemeConfig
const sidebarContext = useSidebar()
const isBrowser = useIsBrowser()
const isApple = isBrowser
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
: true
return (
<NavbarContentLayout
left={
// TODO stop hardcoding items?
<>
{!mobileSidebar.disabled && <NavbarMobileSidebarToggle />}
<NavbarLogo />
<NavbarItems items={leftItems} />
</>
}
right={
// TODO stop hardcoding items?
// Ask the user to add the respective navbar items => more flexible
<div className="flex gap-0.5">
<NavbarItems items={rightItems} />
{hideable && sidebarContext?.hasSidebar && (
<NavbarActions
items={[
{
type: "button",
html: !sidebarContext?.hiddenSidebarContainer
? `<span class="text-compact-x-small-plus">Close sidebar <kbd class="${clsx(
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
"border border-solid rounded border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark font-base text-compact-x-small-plus",
"inline-flex w-[22px] h-[22px] !p-0 justify-center items-center shadow-none ml-0.5"
)}">${isApple ? "⌘" : "Ctrl"}</kbd>
<kbd class="${clsx(
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
"border border-solid rounded border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark font-base text-compact-x-small-plus",
"inline-flex w-[22px] h-[22px] !p-0 justify-center items-center shadow-none"
)}">I</kbd></span>`
: `<span class="text-compact-x-small-plus">Lock sidebar open <kbd class="${clsx(
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
"border border-solid rounded border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark font-base text-compact-x-small-plus",
"inline-flex w-[22px] h-[22px] !p-0 justify-center items-center shadow-none ml-0.5"
)}">${isApple ? "⌘" : "Ctrl"}</kbd>
<kbd class="${clsx(
"bg-medusa-tag-neutral-bg dark:bg-medusa-tag-neutral-bg-dark",
"border border-solid rounded border-medusa-tag-neutral-border dark:border-medusa-tag-neutral-border-dark",
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark font-base text-compact-x-small-plus",
"inline-flex w-[22px] h-[22px] !p-0 justify-center items-center shadow-none"
)}">I</kbd></span>`,
events: {
onClick: sidebarContext?.onCollapse,
onMouseEnter: () => {
if (!sidebarContext?.hiddenSidebarContainer) {
sidebarContext?.setFloatingSidebar(false)
} else {
sidebarContext?.setFloatingSidebar(true)
}
},
onMouseLeave: () => {
setTimeout(() => {
if (
!document.querySelector(
".theme-doc-sidebar-container:hover"
)
) {
sidebarContext?.setFloatingSidebar(false)
}
}, 100)
},
},
Icon: !sidebarContext?.hiddenSidebarContainer ? (
<Sidebar className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
) : (
<ChevronDoubleLeftMiniSolid className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
),
buttonType: "icon",
},
]}
className="sidebar-toggler"
/>
)}
<Tooltip text="Switch theme">
<NavbarColorModeToggle
className={clsx(
"navbar-action-icon-item !w-2 !h-2 [&>button]:!rounded"
)}
/>
</Tooltip>
<NavbarActions items={navbarActions} />
</div>
}
/>
)
}

View File

@@ -0,0 +1,44 @@
import React from "react"
import clsx from "clsx"
import { useThemeConfig } from "@docusaurus/theme-common"
import {
useHideableNavbar,
useNavbarMobileSidebar,
} from "@docusaurus/theme-common/internal"
import { translate } from "@docusaurus/Translate"
import NavbarMobileSidebar from "@theme/Navbar/MobileSidebar"
import type { Props } from "@theme/Navbar/Layout"
export default function NavbarLayout({ children }: Props): JSX.Element {
const {
navbar: { hideOnScroll, style },
} = useThemeConfig()
const mobileSidebar = useNavbarMobileSidebar()
const { navbarRef, isNavbarVisible } = useHideableNavbar(hideOnScroll)
return (
<nav
ref={navbarRef}
aria-label={translate({
id: "theme.NavBar.navAriaLabel",
message: "Main",
description: "The ARIA label for the main navigation",
})}
className={clsx(
"navbar",
"navbar--fixed-top",
hideOnScroll && [
"transition-transform",
!isNavbarVisible && "translate-x-0 translate-y-[calc(-100%-2px)]",
],
{
"navbar--dark": style === "dark",
"navbar--primary": style === "primary",
"navbar-sidebar--show": mobileSidebar.shown,
}
)}
>
{children}
<NavbarMobileSidebar />
</nav>
)
}

View File

@@ -0,0 +1,20 @@
import React from "react"
import Logo from "@theme/Logo"
import MobileLogo from "../../MobileLogo"
export default function NavbarLogo(): JSX.Element {
return (
<>
<Logo
className="navbar__brand hidden lg:block"
imageClassName="navbar__logo"
titleClassName="navbar__title text--truncate"
/>
<MobileLogo
className="navbar__brand lg:hidden mx-auto"
imageClassName="navbar__logo"
titleClassName="navbar__title text--truncate"
/>
</>
)
}

View File

@@ -0,0 +1,42 @@
import React from "react"
import { useNavbarMobileSidebar } from "@docusaurus/theme-common/internal"
import NavbarColorModeToggle from "@theme/Navbar/ColorModeToggle"
import IconClose from "@theme/Icon/Close"
import NavbarLogo from "@theme/Navbar/Logo"
import clsx from "clsx"
function CloseButton() {
const mobileSidebar = useNavbarMobileSidebar()
return (
<button
type="button"
className={clsx(
"bg-transparent border-0 text-inherit cursor-pointer p-0",
"flex ml-auto"
)}
onClick={() => mobileSidebar.toggle()}
>
<IconClose color="var(--ifm-color-emphasis-600)" />
</button>
)
}
export default function NavbarMobileSidebarHeader(): JSX.Element {
return (
<div
className={clsx(
"items-center shadow-navbar dark:shadow-navbar-dark",
"flex flex-1 h-navbar py-0.75 px-1.5"
)}
>
<NavbarLogo />
<NavbarColorModeToggle
className={clsx(
"[&>button]:hover:bg-medusa-button-neutral-hover dark:[&>button]:hover:bg-medusa-button-neutral-hover-dark",
"[&>button]:!rounded"
)}
/>
<CloseButton />
</div>
)
}

View File

@@ -0,0 +1,23 @@
import React from "react"
import clsx from "clsx"
import { useNavbarSecondaryMenu } from "@docusaurus/theme-common/internal"
import type { Props } from "@theme/Navbar/MobileSidebar/Layout"
export default function NavbarMobileSidebarLayout({
primaryMenu,
secondaryMenu,
}: Props): JSX.Element {
const { shown: secondaryMenuShown } = useNavbarSecondaryMenu()
return (
<div className="navbar-sidebar top-[57px] shadow-none">
<div
className={clsx("navbar-sidebar__items", {
"navbar-sidebar__items--show-secondary": secondaryMenuShown,
})}
>
<div className="navbar-sidebar__item menu">{primaryMenu}</div>
<div className="navbar-sidebar__item menu">{secondaryMenu}</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,29 @@
import React from "react"
import { useNavbarMobileSidebar } from "@docusaurus/theme-common/internal"
import { translate } from "@docusaurus/Translate"
import { Sidebar, XMark } from "@medusajs/icons"
export default function MobileSidebarToggle(): JSX.Element {
const { toggle, shown } = useNavbarMobileSidebar()
return (
<button
onClick={toggle}
aria-label={translate({
id: "theme.docs.sidebar.toggleSidebarButtonAriaLabel",
message: "Toggle navigation bar",
description:
"The ARIA label for hamburger menu button of mobile navigation",
})}
aria-expanded={shown}
className="navbar__toggle !block lg:!hidden clean-btn"
type="button"
>
{!shown && (
<Sidebar className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
{shown && (
<XMark className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
)}
</button>
)
}

View File

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

View File

@@ -0,0 +1,55 @@
import React from "react"
import Link from "@docusaurus/Link"
import useBaseUrl from "@docusaurus/useBaseUrl"
import { isRegexpStringMatch } from "@docusaurus/theme-common"
import type { Props } from "@theme/NavbarItem/NavbarNavLink"
export default function NavbarNavLink({
activeBasePath,
activeBaseRegex,
to,
href,
label,
html,
prependBaseUrlToHref,
...props
}: Props): JSX.Element {
// TODO all this seems hacky
// {to: 'version'} should probably be forbidden, in favor of {to: '/version'}
const toUrl = useBaseUrl(to)
const activeBaseUrl = useBaseUrl(activeBasePath)
const normalizedHref = useBaseUrl(href, { forcePrependBaseUrl: true })
// Link content is set through html XOR label
const linkContentProps = html
? { dangerouslySetInnerHTML: { __html: html } }
: {
children: <>{label}</>,
}
if (href) {
return (
<Link
href={prependBaseUrlToHref ? normalizedHref : href}
{...props}
{...linkContentProps}
/>
)
}
return (
<Link
to={toUrl}
isNavLink
{...((activeBasePath || activeBaseRegex) && {
isActive: (_match, location) => {
return activeBaseRegex
? isRegexpStringMatch(activeBaseRegex, location.pathname)
: location.pathname.startsWith(activeBaseUrl)
},
})}
{...props}
{...linkContentProps}
/>
)
}

View File

@@ -0,0 +1,17 @@
import React from "react"
import NavbarSearch from "@theme/Navbar/Search"
import type { Props } from "@theme/NavbarItem/SearchNavbarItem"
import { SearchModalOpener } from "docs-ui"
import { useWindowSize } from "@docusaurus/theme-common"
export default function SearchNavbarItem({
mobile,
}: Props): JSX.Element | null {
const windowSize = useWindowSize()
return (
<NavbarSearch>
<SearchModalOpener isMobile={mobile || windowSize !== "desktop"} />
</NavbarSearch>
)
}

View File

@@ -0,0 +1,86 @@
import React from "react"
import Translate, { translate } from "@docusaurus/Translate"
import { PageMetadata } from "@docusaurus/theme-common"
import Layout from "@theme/Layout"
import useBaseUrl from "@docusaurus/useBaseUrl"
import DocsProviders from "../providers/DocsProviders"
export default function NotFound(): JSX.Element {
return (
<DocsProviders>
<PageMetadata
title={translate({
id: "theme.NotFound.title",
message: "Page Not Found",
})}
/>
<Layout>
<main className="container markdown theme-doc-markdown my-4">
<div className="row">
<div className="col col--6 col--offset-3">
<h1>
<Translate
id="theme.NotFound.title"
description="The title of the 404 page"
>
Page Not Found
</Translate>
</h1>
<p>
<Translate
id="theme.NotFound.p1"
description="The first paragraph of the 404 page"
>
Looks like the page you&apos;re looking for has either changed
into a different location or isn&apos;t in our documentation
anymore.
</Translate>
</p>
<p>
If you think this is a mistake, please{" "}
<a
href="https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml"
rel="noopener noreferrer"
target="_blank"
>
report this issue on GitHub
</a>
</p>
<h2>Some popular links</h2>
<ul>
<li>
<a href={useBaseUrl("/usage/create-medusa-app")}>
Install Medusa with create-medusa-app
</a>
</li>
<li>
<a href="https://docs.medusajs.com/api/store">
Storefront REST API Reference
</a>
</li>
<li>
<a href="https://docs.medusajs.com/api/admin">
Admin REST API Reference
</a>
</li>
<li>
<a href={useBaseUrl("/starters/nextjs-medusa-starter")}>
Install Next.js Storefront
</a>
</li>
<li>
<a href={useBaseUrl("/admin/quickstart")}>
Install Medusa Admin
</a>
</li>
<li>
<a href={useBaseUrl("/user-guide")}>User Guide</a>
</li>
</ul>
</div>
</div>
</main>
</Layout>
</DocsProviders>
)
}

View File

@@ -0,0 +1,51 @@
import React, { useEffect } from "react"
import SearchBar from "@theme-original/SearchBar"
import type SearchBarType from "@theme/SearchBar"
import type { WrapperProps } from "@docusaurus/types"
import useIsBrowser from "@docusaurus/useIsBrowser"
import { useLocation } from "@docusaurus/router"
import { useAnalytics } from "docs-ui"
type Props = WrapperProps<typeof SearchBarType>
export default function SearchBarWrapper(props: Props): JSX.Element {
const isBrowser = useIsBrowser()
const location = useLocation()
const { track } = useAnalytics()
useEffect(() => {
if (isBrowser) {
const trackSearch = (e) => {
if (
!e.target.classList?.contains("DocSearch-Input") &&
!(
e.target.tagName.toLowerCase() === "input" &&
e.target.getAttribute("type") === "search"
)
) {
return
}
const query = e.target.value
if (query.length >= 3) {
// send event to segment
track("search", {
query,
})
}
}
document.body.addEventListener("keyup", trackSearch)
return () => {
document.body.removeEventListener("keyup", trackSearch)
}
}
}, [isBrowser, location.pathname])
return (
<>
<SearchBar {...props} />
</>
)
}

View File

@@ -0,0 +1,12 @@
import { translate } from "@docusaurus/Translate"
import translations from "@theme-original/SearchTranslations"
const changedTranslations = {
...translations,
placeholder: translate({
id: "theme.SearchModal.placeholder",
message: "Find something",
description: "The placeholder of the input of the DocSearch pop-up modal",
}),
}
export default changedTranslations

View File

@@ -0,0 +1,22 @@
import React from "react"
import TOCItems from "@theme-original/TOCItems"
import type TOCItemsType from "@theme/TOCItems"
import type { WrapperProps } from "@docusaurus/types"
import StructuredDataHowTo from "@site/src/components/StructuredData/HowTo"
import { useDoc } from "@docusaurus/theme-common/internal"
import { DocContextValue } from "@medusajs/docs"
type Props = WrapperProps<typeof TOCItemsType>
export default function TOCItemsWrapper(props: Props): JSX.Element {
const { frontMatter, contentTitle } = useDoc() as DocContextValue
return (
<>
<TOCItems {...props} />
{frontMatter?.addHowToData && (
<StructuredDataHowTo toc={props.toc} title={contentTitle} />
)}
</>
)
}

View File

@@ -0,0 +1,261 @@
import React, { ReactElement, cloneElement, useEffect, useRef } from "react"
import clsx from "clsx"
import {
useScrollPositionBlocker,
useTabs,
type TabItemProps,
} from "@docusaurus/theme-common/internal"
import useIsBrowser from "@docusaurus/useIsBrowser"
import type { Props as OldProps } from "@theme/Tabs"
// import styles from "./styles.module.css"
type TabsCustomProps = {
isCodeTabs?: boolean
codeTitle?: string
}
type TabListProps = OldProps & ReturnType<typeof useTabs> & TabsCustomProps
function TabList({
className,
selectedValue,
selectValue,
tabValues,
isCodeTabs = false,
codeTitle,
}: TabListProps) {
const tabRefs: (HTMLLIElement | null)[] = []
const { blockElementScrollPositionUntilNextRender } =
useScrollPositionBlocker()
const codeTabSelectorRef = useRef(null)
const codeTabsWrapperRef = useRef(null)
const handleTabChange = (
event:
| React.FocusEvent<HTMLLIElement>
| React.MouseEvent<HTMLLIElement>
| React.KeyboardEvent<HTMLLIElement>
) => {
const newTab = event.currentTarget
const newTabIndex = tabRefs.indexOf(newTab)
const newTabValue = tabValues[newTabIndex]!.value
if (newTabValue !== selectedValue) {
blockElementScrollPositionUntilNextRender(newTab)
selectValue(newTabValue)
}
}
const handleKeydown = (event: React.KeyboardEvent<HTMLLIElement>) => {
let focusElement: HTMLLIElement | null = null
switch (event.key) {
case "Enter": {
handleTabChange(event)
break
}
case "ArrowRight": {
const nextTab = tabRefs.indexOf(event.currentTarget) + 1
focusElement = tabRefs[nextTab] ?? tabRefs[0]!
break
}
case "ArrowLeft": {
const prevTab = tabRefs.indexOf(event.currentTarget) - 1
focusElement = tabRefs[prevTab] ?? tabRefs[tabRefs.length - 1]!
break
}
default:
break
}
focusElement?.focus()
}
const changeTabSelectorCoordinates = (selectedTab) => {
if (!codeTabSelectorRef?.current || !codeTabsWrapperRef?.current) {
return
}
const selectedTabsCoordinates = selectedTab.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 selectedTab = tabRefs.find(
(tab) => tab.getAttribute("aria-selected") === "true"
)
if (selectedTab) {
changeTabSelectorCoordinates(selectedTab)
}
}
}, [codeTabSelectorRef, tabRefs])
return (
<div
className={clsx(isCodeTabs && "code-header", !isCodeTabs && "[&+*]:pt-2")}
>
<div
className={clsx(isCodeTabs && "relative overflow-auto")}
ref={codeTabsWrapperRef}
>
{isCodeTabs && (
<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-0 xs:z-[1] xs:rounded-full"
)}
ref={codeTabSelectorRef}
></span>
)}
<ul
role="tablist"
aria-orientation="horizontal"
className={clsx(
"tabs",
isCodeTabs && "no-scrollbar",
"list-none",
className
)}
>
{tabValues.map(({ value, label, attributes }) => (
<li
// TODO extract TabListItem
role="tab"
tabIndex={selectedValue === value ? 0 : -1}
aria-selected={selectedValue === value}
key={value}
ref={(tabControl) => tabRefs.push(tabControl)}
onKeyDown={handleKeydown}
onClick={handleTabChange}
{...attributes}
className={clsx(
isCodeTabs &&
"text-compact-small-plus py-0.25 border border-solid border-transparent whitespace-nowrap rounded-full [&:not(:first-child)]:ml-0.25",
"!mt-0 cursor-pointer",
attributes?.className,
isCodeTabs && "z-[2] flex justify-center items-center",
isCodeTabs &&
selectedValue !== value &&
"text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark hover:!bg-medusa-code-bg-base dark:hover:!bg-medusa-code-bg-base-dark",
isCodeTabs &&
selectedValue === value &&
"text-medusa-code-text-base dark:text-medusa-code-text-base-dark border border-solid border-medusa-code-border dark:border-medusa-code-border-dark bg-medusa-code-bg-base dark:bg-medusa-code-bg-base-dark xs:!border-none xs:!bg-transparent",
!isCodeTabs &&
"border-0 border-b-[3px] rounded inline-flex p-1 transition-[background-color] duration-200 ease-ease",
!isCodeTabs &&
selectedValue === value &&
"border-solid border-medusa-fg-base dark:border-medusa-fg-base-dark rounded-b-none",
!isCodeTabs &&
selectedValue !== value &&
"text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark",
(!isCodeTabs || !attributes?.badge) && "px-0.75",
isCodeTabs &&
attributes?.badge &&
"[&_.badge]:ml-0.5 [&_.badge]:py-0.125 [&_.badge]:px-[6px] [&_.badge]:rounded-full pl-0.75 pr-0.25"
)}
>
{label ?? value}
</li>
))}
</ul>
</div>
{isCodeTabs && (
<span
className={clsx(
"text-compact-small-plus text-medusa-code-text-subtle dark:text-medusa-code-text-subtle-dark hidden xs:block"
)}
>
{codeTitle}
</span>
)}
</div>
)
}
function TabContent({
lazy,
children,
selectedValue,
}: OldProps & ReturnType<typeof useTabs>) {
const childTabs = (Array.isArray(children) ? children : [children]).filter(
Boolean
) as ReactElement<TabItemProps>[]
if (lazy) {
const selectedTabItem = childTabs.find(
(tabItem) => tabItem.props.value === selectedValue
)
if (!selectedTabItem) {
// fail-safe or fail-fast? not sure what's best here
return null
}
return cloneElement(selectedTabItem)
}
return (
<div>
{childTabs.map((tabItem, i) =>
cloneElement(tabItem, {
key: i,
hidden: tabItem.props.value !== selectedValue,
})
)}
</div>
)
}
type TabsComponentProp = TabsCustomProps & OldProps
function TabsComponent(props: TabsComponentProp): JSX.Element {
const tabs = useTabs(props)
return (
<div className={clsx("mb-1.5")}>
<TabList {...props} {...tabs} />
<TabContent {...props} {...tabs} />
</div>
)
}
type TabsProps = {
wrapperClassName?: string
isCodeTabs?: boolean
} & OldProps
function checkCodeTabs(props: TabsProps): boolean {
return props.groupId === "npm2yarn" || props.isCodeTabs
}
export default function Tabs(props: TabsProps): JSX.Element {
const isBrowser = useIsBrowser()
useEffect(() => {
if (!window.localStorage.getItem("docusaurus.tab.npm2yarn")) {
// set the default
window.localStorage.setItem("docusaurus.tab.npm2yarn", "yarn")
}
}, [])
const isCodeTabs = checkCodeTabs(props)
return (
<div
className={clsx(
"tabs-wrapper",
props.wrapperClassName,
isCodeTabs && "code-tabs",
!isCodeTabs &&
"bg-docs-bg-surface dark:bg-docs-bg-surface-dark p-1 border border-solid border-medusa-border-base dark:border-medusa-border-base-dark rounded"
)}
>
<TabsComponent
// Remount tabs after hydration
// Temporary fix for https://github.com/facebook/docusaurus/issues/5653
key={String(isBrowser)}
isCodeTabs={isCodeTabs}
{...props}
/>
</div>
)
}

View File

@@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const vsCodeTheme = require("prism-react-renderer/themes/vsDark")
const theme = {
...vsCodeTheme,
plain: {
...vsCodeTheme.plain,
backgroundColor: "#111827",
},
}
module.exports = theme

6
www/apps/docs/src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
analytics?: any
}
}

202
www/apps/docs/src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,202 @@
declare module "@theme/CodeBlock" {
import type { Props as DocusaurusProps } from "@theme/CodeBlock"
export interface Props extends DocusaurusProps {
readonly noReport?: boolean
readonly noCopy?: boolean
}
}
declare module "@theme/CodeBlock/Content/String" {
import type { Props as CodeBlockProps } from "@theme/CodeBlock"
export interface Props extends Omit<CodeBlockProps, "children"> {
readonly children: string
readonly noReport?: boolean
readonly noCopy?: boolean
}
export default function CodeBlockStringContent(props: Props): JSX.Element
}
declare module "@medusajs/docs" {
import type { ThemeConfig as DocusaurusThemeConfig } from "@docusaurus/preset-classic"
import type { DocusaurusConfig } from "@docusaurus/types"
import type {
PropSidebarItemCategory,
PropSidebarItemLink,
PropSidebarItemHtml,
} from "@docusaurus/plugin-content-docs"
import { BadgeProps, ButtonType, ButtonVariants } from "docs-ui"
import { IconProps } from "@medusajs/icons/dist/types"
import { DocContextValue as DocusaurusDocContextValue } from "@docusaurus/theme-common/internal"
import { ReactNode } from "react"
import { NavbarLogo } from "@docusaurus/theme-common"
import type { DocusaurusContext } from "@docusaurus/types"
type ItemCustomProps = {
customProps?: {
themedImage: {
light: string
dark?: string
}
image?: string
icon?: React.FC<IconProps>
iconName?: string
description?: string
className?: string
isSoon?: boolean
badge: BadgeProps
html?: string
sidebar_icon?: string
sidebar_is_title?: boolean
sidebar_is_soon?: boolean
sidebar_is_group_headline?: boolean
sidebar_is_group_divider?: boolean
sidebar_is_divider_line?: boolean
sidebar_is_back_link?: boolean
}
}
export declare type ModifiedPropSidebarItemCategory =
PropSidebarItemCategory & ItemCustomProps
export declare type ModifiedPropSidebarItemLink = PropSidebarItemLink &
ItemCustomProps
export declare type ModifiedPropSidebarItemHtml = PropSidebarItemHtml &
ItemCustomProps
export declare type ModifiedSidebarItem =
| ModifiedPropSidebarItemCategory
| ModifiedPropSidebarItemLink
| ModifiedPropSidebarItemHtml
export declare type ModifiedDocCardBase = {
type: string
href: string
icon: ReactNode | string
title: string
description?: string
html?: string
containerClassName?: string
isSoon?: boolean
badge?: BadgeProps
children?: React.ReactNode
}
export declare type ModifiedDocCardItemLink = {
type: "link"
} & ModifiedDocCardBase &
ModifiedPropSidebarItemLink
export declare type ModifiedDocCardItemCategory = {
type: "category"
} & ModifiedDocCardBase &
ModifiedPropSidebarItemCategory
export declare type ModifiedDocCard =
| ModifiedDocCardItemLink
| ModifiedDocCardItemCategory
export declare type SocialLink = {
href: string
type: string
}
export declare type NavbarActionBase = {
type: string
title?: string
icon?: string
Icon?: React.ReactElement
className?: string
label?: string
html?: string
}
export declare type NavbarActionLink = NavbarActionBase & {
type: "link"
href: string
}
export declare type NavbarActionButton = NavbarActionBase & {
type: "button"
variant?: ButtonVariants
buttonType?: ButtonType
href?: string
events?: {
onClick?: MouseEventHandler<HTMLButtonElement>
onMouseEnter?: MouseEventHandler<HTMLButtonElement>
onMouseLeave?: MouseEventHandler<HTMLButtonElement>
onMouseOver?: MouseEventHandler<HTMLButtonElement>
}
}
export declare type NavbarAction = NavbarActionLink | NavbarActionButton
export declare type OptionType = {
value: string
label: string
isAllOption?: boolean
}
export declare type ThemeConfig = {
reportCodeLinkPrefix?: string
footerFeedback: {
event: string
}
socialLinks?: SocialLink[]
cloudinaryConfig?: {
cloudName?: string
flags?: string[]
resize?: {
action: string
width?: number
height?: number
aspectRatio?: string
}
roundCorners?: number
}
navbarActions: NavbarAction[]
// resolve type errors
prism: {
magicComments: MagicCommentConfig[]
}
mobileLogo: NavbarLogo
algoliaConfig?: {
apiKey: string
indexNames: {
docs: string
api: string
}
appId: string
filters: OptionType[]
defaultFilters: string[]
defaultFiltersByPath: {
path: string
filters: string[]
}[]
}
analytics?: {
apiKey: string
}
} & DocusaurusThemeConfig
export declare type MedusaDocusaurusConfig = {
themeConfig: ThemeConfig
} & DocusaurusConfig
export declare type DocContextValue = {
frontMatter: {
addHowToData?: boolean
badge?: {
variant: string
text: string
}
}
} & DocusaurusDocContextValue
export declare type MedusaDocusaurusContext = DocusaurusContext & {
siteConfig: MedusaDocusaurusConfig
}
}

View File

@@ -0,0 +1,33 @@
import {
PropSidebarItem,
PropSidebarItemLink,
} from "@docusaurus/plugin-content-docs"
export default function filterListItems(
items: PropSidebarItemLink[],
pathPattern: string | RegExp
): PropSidebarItemLink[] {
if (!items.length) {
return items
}
const pattern = new RegExp(pathPattern)
return items.filter((item: PropSidebarItemLink) => pattern.test(item.href))
}
/**
* Flatting a sidebar list moving items from category
* to links
*/
export function flattenList(items: PropSidebarItem[]): PropSidebarItem[] {
const newItems = items.map((item: PropSidebarItem) => {
if (item.type !== "category") {
return item
}
return item.items
})
return newItems.flat()
}

View File

@@ -0,0 +1,14 @@
import {
findSidebarCategory,
useDocsSidebar,
} from "@docusaurus/theme-common/internal"
import { PropSidebarItem } from "@docusaurus/plugin-content-docs"
export default function getFirstCategoryItem(
categoryLabel: string
): PropSidebarItem | undefined {
return findSidebarCategory(
useDocsSidebar().items,
(item) => item.label === categoryLabel
)?.items[0]
}

View File

@@ -0,0 +1,8 @@
export default function getOsShortcut() {
const isMacOs =
typeof navigator !== "undefined"
? navigator.userAgent.toLowerCase().indexOf("mac") !== 0
: true
return isMacOs ? "⌘" : "Ctrl"
}

View File

@@ -0,0 +1,192 @@
export type GlossaryType = {
matchTextRegex: RegExp
ignoreTextRegex?: RegExp
title: string
content: string
referenceLink: string
}
const glossary: GlossaryType[] = [
{
title: "Migration",
content: "A script that is used to reflect changes to the database schema.",
matchTextRegex: /migration/i,
referenceLink: "/development/entities/migrations/overview",
},
{
title: "Repository",
content:
"A class that provides generic methods to retrieve and manipulate entities.",
matchTextRegex: /(repository|repositories)/i,
referenceLink: "/development/entities/repositories",
},
{
title: "Entity",
content: "A class that represents a table in the database.",
matchTextRegex: /(entity|entities)/i,
referenceLink: "/development/entities/overview",
},
{
title: "Dependency Injection",
content:
"Classes receive other resources, such as services or repositories, in their constructor using dependency injection.",
matchTextRegex: /(dependency injection|dependency-injection)/i,
referenceLink: "/development/fundamentals/dependency-injection",
},
{
title: "Middleware",
content:
"A function that can be executed before or after endpoint requests are handled.",
matchTextRegex: /middleware/i,
referenceLink: "/development/endpoints/add-middleware",
},
{
title: "Endpoint",
content: "REST API routes exposed to frontends or external systems.",
matchTextRegex: /endpoint/i,
referenceLink: "/development/endpoints/overview",
},
{
title: "Subscriber",
content: "A class that registers handler methods to an event.",
matchTextRegex: /subscriber/i,
referenceLink: "/development/events/subscribers",
},
{
title: "Module",
content:
"Reusable pieces of code, typically as NPM packages, that provide specific functionality or feature.",
matchTextRegex: /module/,
ignoreTextRegex: /commerce module/i,
referenceLink: "/development/modules/overview",
},
{
title: "Loader",
content: "A script that runs when the Medusa backend starts.",
matchTextRegex: /loader/i,
referenceLink: "/development/loaders/overview",
},
{
title: "Scheduled Job",
content:
"A task that is performed at specific times during the Medusa backend's runtime.",
matchTextRegex: /scheduled job/i,
referenceLink: "/development/scheduled-jobs/overview",
},
{
title: "Batch Job",
content: "A task that is performed asynchronously and iteratively.",
matchTextRegex: /batch job/i,
referenceLink: "/development/batch-jobs",
},
{
title: "Strategy",
content:
"A class that contains an isolated piece of logic that can be overridden and customized.",
matchTextRegex: /(strategy|strategies)/i,
referenceLink: "/development/strategies/overview",
},
{
title: "Feature Flag",
content:
"A flag that guards beta features in the Medusa backend and ensures they can only be used when enabled.",
matchTextRegex: /(feature-flag|feature flag)/i,
referenceLink: "/development/feature-flags/overview",
},
{
title: "Idempotency Key",
content:
"A unique, randomly-generated key associated with a process, such as cart completion.",
matchTextRegex: /(idempotency-key|idempotency key)/i,
referenceLink: "/development/idempotency-key/overview",
},
{
title: "Search Service",
content:
"A class that implements an interface to provide search functionalities.",
matchTextRegex: /(search service|search-service)/i,
referenceLink: "/development/search/overview",
},
{
title: "File Service",
content:
"A class that implements an interface to provide storage functionalities.",
matchTextRegex: /(file service|file-service)/i,
referenceLink: "/development/file-service/overview",
},
{
title: "Notification Service",
content:
"A class that implements an interface to provide notification functionalities.",
matchTextRegex: /(notification service|notification-service)/i,
referenceLink: "/development/notification/overview",
},
{
title: "Plugin",
content:
"A reusable NPM package that can be reused in Medusa backends and provide custom functionalities.",
matchTextRegex: /plugin/i,
ignoreTextRegex:
/(file-service plugin|file service plugin|notification service plugin|notification-service plugin|search service plugin|search-service plugin)/i,
referenceLink: "/development/plugins/overview",
},
{
title: "Service",
content:
"A class that typically includes helper methods associated with an entity.",
matchTextRegex: /service/i,
ignoreTextRegex:
/(file-service|file service|notification service|notification-service|search service|search-service)/i,
referenceLink: "/development/services/overview",
},
{
title: "Publishable API Key",
content:
"An API key that is associated with a set of resources and used on the client (storefront) side.",
matchTextRegex: /(publishable-api-key|publishable api key)/i,
referenceLink: "/development/publishable-api-keys",
},
{
title: "JavaScript Client",
content:
"An NPM package that provides methods to interact with the Medusa backend's REST APIs.",
matchTextRegex: /(js-client|js client|medusa javascript client)/i,
referenceLink: "/js-client/overview",
},
{
title: "Medusa React",
content:
"An NPM package that provides React hooks and utility methods to interact with the Medusa backend's REST APIs.",
matchTextRegex: /(medusa-react|medusa react)/i,
referenceLink: "/medusa-react/overview",
},
{
title: "Admin Widget",
content:
"Custom React components that can be injected into different locations in the Medusa admin dashboard.",
matchTextRegex: /admin widget/i,
referenceLink: "/admin/widgets",
},
{
title: "Admin UI Route",
content:
"A React component that is used to create a new page in the Medusa admin dashboard.",
matchTextRegex: /(admin route|admin UI route)/i,
referenceLink: "/admin/routes",
},
{
title: "Admin Setting Page",
content:
"A React component that is used to create a new setting page in the Medusa admin dashboard.",
matchTextRegex: /admin setting page/i,
referenceLink: "/admin/setting-pages",
},
]
export const getGlossary = () => [...glossary]
export const getGlossaryByPath = (path: string): GlossaryType | undefined => {
return glossary.find(
(g) => g.matchTextRegex.test(path) && !g.ignoreTextRegex?.test(path)
)
}

View File

@@ -0,0 +1,797 @@
import React from "react"
import { LearningPathType } from "../providers/LearningPath"
import Link from "@docusaurus/Link"
const paths: LearningPathType[] = [
{
name: "simple-quickstart",
label: "Start Selling in 3 Steps",
description: "Create and deploy a full-fledged ecommerce store.",
steps: [
{
title: "Create a Next.js Starter Template",
description:
"Create a Next.js Starter Template and connect it to your Medusa backend.",
path: "/starters/nextjs-medusa-starter",
},
{
title: "Deploy the backend",
path: "/deployments/server/deploying-on-railway",
descriptionJSX: (
<>
Deploy your backend to Railway. You can alternatively check out{" "}
<Link href="/deployments/server">other deployment guides</Link>
</>
),
},
{
title: "Deploy the storefront",
description: "Deploy your storefront to your preferred hosting.",
path: "/deployments/storefront",
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on building your store!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_simple-quickstart",
},
},
},
{
name: "marketplace",
label: "Build a marketplace",
description:
"Customize the backend and handle events to build a marketplace.",
steps: [
{
title: "Extend entities",
descriptionJSX: (
<>
Extend entities, such as <code>User</code> or <code>Product</code>{" "}
entites, to associate them with the <code>Store</code> entity.
</>
),
path: "/development/entities/extend-entity",
},
{
title: "Access logged-in user",
description:
"Create a middleware that registers the logged-in user in the dependency container.",
path: "/development/endpoints/example-logged-in-user",
},
{
title: "Extend services",
descriptionJSX: (
<>
Extend services, such as <code>ProductService</code> to customize
data management functionalities
</>
),
path: "/development/services/extend-service",
},
{
title: "Handle events",
descriptionJSX: (
<>
Listen to events like <code>order.placed</code> and handle them with
subscribers
</>
),
path: "/development/events/create-subscriber",
},
{
title: "Add Payment Provider",
path: "/plugins/payment",
descriptionJSX: (
<>
Add a payment provider to your Medusa backend. You can choose to
install a plugin or{" "}
<Link href="/modules/carts-and-checkout/backend/add-payment-provider">
create your own provider
</Link>
.
</>
),
},
{
title: "Customize Admin",
path: "/admin/widgets",
descriptionJSX: (
<>
As you add marketplace features to your store, you&apos;ll most
likely need to customize the admin to provide an interface to manage
these features.
<br />
You can extend the admin plugin to add widgets,{" "}
<Link to="/admin/routes">UI routes</Link>, or{" "}
<Link to="/admin/setting-pages">setting pages</Link>.
</>
),
},
{
title: "Implement Role-Based Access Control",
path: "/modules/users/backend/rbac",
description:
"In your marketplace, you may need to implement role-based access control (RBAC) within stores. This will restrict some users' permissions to specified functionalities or endpoints.",
},
{
title: "Create a storefront",
path: "/starters/nextjs-medusa-starter",
descriptionJSX: (
<>
Build a storefront either using the Next.js Starter Template or{" "}
<Link href="/storefront/roadmap">from scratch</Link>.
</>
),
},
{
title: "Deploy the backend",
path: "/deployments/server/deploying-on-railway",
descriptionJSX: (
<>
Deploy your backend to Railway. You can alternatively check out{" "}
<Link href="/deployments/server">other deployment guides</Link>
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on building your marketplace!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_marketplace",
},
},
},
{
name: "subscriptions",
label: "Build Subscription-based Purchases",
description:
"Customize the backend and handle events to implement subscriptions",
steps: [
{
title: "Extend entities",
path: "/development/entities/extend-entity",
descriptionJSX: (
<>
Extend entities, such as the <code>Order</code> entity, to associate
them with the <code>Store</code> entity. You can also{" "}
<Link href="/development/entities/create">
Create a custom entity
</Link>
.
</>
),
},
{
title: "Handle events",
descriptionJSX: (
<>
Create a subscriber that listens to the <code>order.placed</code>{" "}
event, or other{" "}
<Link href="/development/events/events-list">events</Link>, and
handles creating the subscription in Medusa.
</>
),
path: "/development/events/create-subscriber",
},
{
title: "Create a Scheduled Job",
description:
"Create a scheduled job that checks daily for subscriptions that needs renewal.",
path: "/development/scheduled-jobs/create",
},
{
title: "Customize Admin",
path: "/admin/widgets",
descriptionJSX: (
<>
As you add subscription features to your store, you may need to
customize the admin to provide an interface to manage these
features.
<br />
You can extend the admin plugin to add widgets,{" "}
<Link to="/admin/routes">UI routes</Link>, or{" "}
<Link to="/admin/setting-pages">setting pages</Link>.
</>
),
},
{
title: "Create a storefront",
path: "/starters/nextjs-medusa-starter",
descriptionJSX: (
<>
Build a storefront either using the Next.js Starter Template or{" "}
<Link href="/storefront/roadmap">from scratch</Link>.
</>
),
},
{
title: "Deploy the backend",
path: "/deployments/server/deploying-on-railway",
descriptionJSX: (
<>
Deploy your backend to Railway. You can alternatively check out{" "}
<Link href="/deployments/server">other deployment guides</Link>
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on implementing subscription-based purchases!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_subscriptions",
},
},
},
{
name: "b2b",
label: "Build a B2B store",
description:
"Utilize Medusa's features and customization capabilities to build a B2B store.",
steps: [
{
title: "Create a B2B Sales Channel",
path: "/user-guide/sales-channels/manage",
descriptionJSX: (
<>
You can create a B2B sales channel that will include only your
wholesale products.
<br />
You can either use the Medusa admin, or the{" "}
<Link to="/modules/sales-channels/admin/manage">
Admin REST APIs
</Link>
.
</>
),
},
{
title: "Create a Publishable API Key",
path: "/user-guide/settings/publishable-api-keys",
descriptionJSX: (
<>
Publishable API keys can be associated with one or more sales
channels. You can then use the publishable API key in your
storefront or client.
<br />
You can either use the Medusa admin, or the{" "}
<Link to="/development/publishable-api-keys/admin/manage-publishable-api-keys">
Admin REST APIs
</Link>
.
</>
),
},
{
title: "Add Wholesale Products",
path: "/user-guide/products/manage",
descriptionJSX: (
<>
You can add your wholesale products and make them only available in
the B2B sales channel.
<br />
You can use the Medusa admin to add the products. Other alternatives
are:
<ul>
<li>
<Link to="/modules/products/admin/manage-products">
Add Products Using REST APIs
</Link>
</li>
<li>
<Link to="/user-guide/products/import">
Import Products Using Medusa Admin
</Link>
</li>
<li>
<Link to="/modules/products/admin/import-products">
Import Products Using REST APIs
</Link>
</li>
</ul>
</>
),
},
{
title: "Create a B2B Customer Group",
path: "/user-guide/customers/groups",
descriptionJSX: (
<>
Customer groups can be used to apply different prices for different
subsets of customers, in this case B2B customers.
<br />
You can either use the Medusa admin, or the{" "}
<Link to="/modules/customers/admin/manage-customer-groups">
Admin REST APIs
</Link>
.
</>
),
},
{
title: "Add B2B Customers",
path: "/user-guide/customers/manage",
descriptionJSX: (
<>
You can now add B2B customers and assign them to the B2B customer
group. Alternatively, if you want to allow B2B customers to register
themselves, you can implement that logic within your storefront.
<br />
You can either use the Medusa admin, or the{" "}
<Link to="/modules/customers/admin/manage-customers">
Admin REST APIs
</Link>
.
</>
),
},
{
title: "Create B2B Price Lists",
path: "/user-guide/price-lists/manage",
descriptionJSX: (
<>
A price list allows you to set different prices on a set of products
for different conditions. You can use this when building a B2B store
to assign different prices for B2B customer groups.
<br />
You can use the Medusa admin to add the price list. Other
alternatives are:
<ul>
<li>
<Link to="/modules/price-lists/admin/manage-price-lists">
Add Price List Using REST APIs
</Link>
</li>
<li>
<Link to="/user-guide/price-lists/import">
Import Prices Using Medusa Admin
</Link>
</li>
<li>
<Link to="/modules/price-lists/admin/import-prices">
Import Prices Using REST APIs
</Link>
</li>
</ul>
</>
),
},
{
title: "Create Custom Entities",
path: "/development/entities/create",
descriptionJSX: (
<>
Your use case may be more elaborate than what is shown in this
recipe.
<br />
Medusa can be customized to add custom entities, endpoints,
services, and more.
<br />
You can find additional development resources in the{" "}
<Link to="/development/overview">Medusa development section</Link>.
</>
),
},
{
title: "Create an Endpoint to Check Customers",
path: "/development/entities/create",
descriptionJSX: (
<>
On the clients communicating with your store, such as the
storefront, youll need to check if the currently logged-in customer
is a normal customer or a B2B customer.
<br />
To do that, you need to create a custom endpoint that handles the
checking based on the custom logic you&apos;ve chosen to indicate a
customer is a B2B customer.
</>
),
},
{
title: "Customize Admin",
path: "/admin/widgets",
descriptionJSX: (
<>
As you add B2B features to your store, you may need to customize the
admin to provide an interface to manage these features.
<br />
You can extend the admin plugin to add widgets,{" "}
<Link to="/admin/routes">UI routes</Link>, or{" "}
<Link to="/admin/setting-pages">setting pages</Link>.
</>
),
},
{
title: "Customize Storefront",
path: "/starters/nextjs-medusa-starter",
descriptionJSX: (
<>
You may need to customize your storefront to add different
interfaces for B2B and regular customers, or show products
differently.
<br />
You can customize the Next.js storefront, or you can{" "}
<Link to="/storefront/roadmap">build a custom storefront</Link>.
<br />
In your storefront, make sure to{" "}
<Link to="/development/publishable-api-keys/storefront/use-in-requests">
use publishable API keys
</Link>{" "}
in your requests.
</>
),
},
{
title: "Deploy the B2B store",
path: "/deployments/server",
descriptionJSX: (
<>
Once you finish your development, you can deploy your B2B backend to
your preferred hosting provider. You can also{" "}
<Link to="/deployments/storefront">deploy your storefront</Link> to
your preferred hosting provider.
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on building a B2B store!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_b2b",
},
},
},
// TODO: Eventually remove these learning paths
{
name: "rbac",
label: "Role-based access control (RBAC)",
description: "Implement roles and permissions for admin users in Medusa",
steps: [
{
title: "Create Role and Permission Entities",
path: "/development/entities/create",
description:
"When implementing RBAC, you typically require the availability of roles and permissions, both of which would require new entities. A role would include different permissions, such as the ability to access the products route, and it can be assigned to one or more users.",
},
{
title: "Extend Entities",
path: "/development/entities/extend-entity",
descriptionJSX: (
<>
To associate roles with users, you need to extend the{" "}
<code>User</code> entity to add the relation between it and the new{" "}
<code>Role</code> entity. You can also extend other entities that
are associated with your custom one, such as the <code>Store</code>{" "}
entity.
</>
),
},
{
title: "Create Guard Middleware",
path: "/development/endpoints/add-middleware",
description:
"To ensure that users who have the privilege can access an endpoint, you must create a middleware that guards admin routes. This middleware will run on all authenticated admin requests to ensure that only allowed users can access an endpoint.",
},
{
title: "Create Services",
path: "/development/services/create-service",
descriptionJSX: (
<>
For every entity you create, such as the <code>Role</code> and{" "}
<code>Permission</code> entities, you must create a service that
provides create, read, update, and delete (CRUD) functionalities at
the very least.
<br />
If you also extended entities, such as the <code>User</code> entity,
you may need to{" "}
<Link to="/development/services/extend-service">
extend its core service
</Link>{" "}
<code>UserService</code> as well to perform custom functionalities
related to your implementation.
</>
),
},
{
title: "Create Endpoints",
path: "/development/endpoints/create",
descriptionJSX: (
<>
To manage the roles and permissions, youll need to create custom
endpoints, typically for Create, Read, Update, and Delete (CRUD)
operations.
<br />
After creating the endpoints, you may test adding roles and
permissions, and how they provide different access for different
roles and users.
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on implementing RBAC!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_rbac",
},
},
},
{
name: "entity-and-api",
label: "Create Entity and Expose it with Endpoints",
description:
"Learn how to create a new table in your database, then create endpoints to expose and manipulate its data.",
steps: [
{
title: "Create entity",
path: "/development/entities/create",
description: "Create your entity, its migration, and repository.",
},
{
title: "Create service",
path: "/development/services/create-service",
description:
"A service is a class that defines helper methods for your entity. The service will be used by the endpoints to access or modify the entity's data.",
},
{
title: "Create endpoints",
path: "/development/endpoints/create",
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on creating your entity and endpoints!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_entity-and-api",
},
},
},
{
name: "storefront",
label: "Create a Custom Storefront",
description:
"Learn how to create a custom storefront with your preferred language or framework.",
steps: [
{
title: "Choose your client",
path: "/medusa-react/overview",
descriptionJSX: (
<>
As your storefront connect to the Medusa backend, you need a way to
interact with the backend&apos;s REST APIs. There are three ways to
do so, based on your type of project:
<ul>
<li>
<Link to="/medusa-react/overview">Medusa React</Link>: Can be
used in any React-based project. For example, in a Next.js
storefront.
</li>
<li>
<Link to="/js-client/overview">Medusa JS Client</Link>: Can be
used in any JavaScript and NPM based project. For example, in a
Nuxt storefront.
</li>
<li>
<Link to={`https://docs.medusajs.com/api/store`}>
Store REST APIs
</Link>
: You can send requests directly to the API endpoints without
using Medusa&apos;s clients.
</li>
</ul>
</>
),
},
{
title: "Set CORS configurations in Backend",
path: "/development/backend/configurations#admin_cors-and-store_cors",
description:
"To ensure your storefront can connect to the backend, make sure to configure the backend's CORS configuration based on your storefront's local or remote URL.",
},
{
title: "Create a Publishable API Key",
path: "/user-guide/settings/publishable-api-keys",
descriptionJSX: (
<>
A publishable API key allows you to associate a key with a sales
channel. Then, you can include that key in the headers of all your
requests.
<br />
You can create the publishable API key from the dashboard.
Alternatively, you can create it using the{" "}
<Link to="/development/publishable-api-keys/admin/manage-publishable-api-keys">
Admin APIs
</Link>
</>
),
},
{
title: "Use Publishable API Key",
path: "/development/publishable-api-keys/storefront/use-in-requests",
description:
"After creating the publishable API key and associating it with sales channels, you can pass it in the header of your requests to Store API endpoints.",
},
{
title: "Add Region Selection",
path: "/modules/regions-and-currencies/storefront/use-regions",
description:
"In your storefront, you can allow your customers to view available regions and select their current region. This can affect the prices, discounts, and shipping and payment providers available to the customer.",
},
{
title: "Display Products",
path: "/modules/products/storefront/show-products",
description: "Display products to your customers in the storefront.",
},
{
title: "Implement Cart Functionalities",
path: "/modules/carts-and-checkout/storefront/implement-cart",
description:
"Allow your customers to add items to their cart, update them, and more in preparation for checkout.",
},
{
title: "Implement Checkout Flow",
path: "/modules/carts-and-checkout/storefront/implement-checkout-flow",
description:
"Implement the checkout flow that allows customers to handle shipping and payment, then place their orders.",
},
{
title: "Implement Customer Profiles",
path: "/modules/customers/storefront/implement-customer-profiles",
description:
"Allow customers to register, login, edit their profile information, and more.",
},
{
title: "More Commerce Functionalities",
path: "/modules/overview",
descriptionJSX: (
<>
This recipe guided you to create a storefront with basic
functionalities. You can add more functionalities to your storefront
based on your use case.
<ul>
<li>
The <Link to="/modules/overview">Commerce Modules</Link>{" "}
documentation holds various storefront-related how-to guides to
help you implement different features.
</li>
<li>
You can also checkout the{" "}
<Link to={`https://docs.medusajs.com/api/store`}>
Store REST APIs
</Link>{" "}
for a full list of available REST APIs.
</li>
</ul>
</>
),
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on creating your storefront!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_storefront",
},
},
},
{
name: "plugin",
label: "Create a Plugin",
description:
"Learn how to create a plugin that can be re-used across Medusa backends.",
steps: [
{
title: "Setup plugin project",
path: "/development/backend/install",
description:
"A plugin is initially a Medusa backend with customizations. If you don't have a project ready, you can create one using Medusa's CLI tool.",
},
{
title: "Implement Customizations",
path: "/development/entities/create",
descriptionJSX: (
<>
Your plugin can hold backend and admin customizations. Those
include:
<ul>
<li>
<Link to="/development/entities/create">Create Entity</Link>
</li>
<li>
<Link to="/development/services/create-service">
Create Service
</Link>
</li>
<li>
<Link to="/development/endpoints/create">
Create an Endpoint
</Link>
</li>
<li>
<Link to="/development/events/create-subscriber">
Create Subscriber
</Link>
</li>
<li>
<Link to="/admin/widgets">Create Admin Widgets</Link>
</li>
<li>
<Link to="/admin/routes">Create Admin Routes</Link>
</li>
<li>
<Link to="/admin/setting-pages">
Create Admin Setting Pages
</Link>
</li>
<li>
<Link to="/development/search/create">
Create Search Service
</Link>
</li>
<li>
<Link to="/development/file-service/create-file-service">
Create File Service
</Link>
</li>
<li>
<Link to="/development/notification-service/create-notification-service">
Create Notification Service
</Link>
</li>
</ul>
If you&apos;ve already made your custom development, you can skip to
the next step.
</>
),
},
{
title: "Change your package.json",
path: "/development/plugins/create#changes-to-packagejson",
descriptionJSX: (
<>
Once you&apos;re done making your customizations and you&apos;re
ready to publish your plugin, make changes to your{" "}
<code>package.json</code> in preparation for publishing.
</>
),
},
{
title: "Optionally test locally",
path: "/development/plugins/create#test-your-plugin",
description:
"If necessary, you can test your plugin in a separate local Medusa backend. It's recommended, however, to do your plugin testing within the plugin project.",
},
{
title: "Publish plugin",
path: "/development/plugins/publish",
description: "Publish your plugin on NPM.",
},
],
finish: {
type: "rating",
step: {
title: "Congratulations on creating your plugin!",
description: "Please rate your experience using this recipe.",
eventName: "rating_path_plugin",
},
},
},
]
// get a shallow copy
export const getLearningPaths = () => [...paths]
export const getLearningPath = (
pathName: string
): LearningPathType | undefined => paths.find((path) => path.name === pathName)

View File

@@ -0,0 +1,27 @@
function reverseSidebarItems(sidebarItems, categoryItem) {
let result = sidebarItems
if (categoryItem.customProps?.reverse) {
// Reverse items in categories
result = result.map((item) => {
if (item.type === "category") {
return { ...item, items: reverseSidebarItems(item.items, categoryItem) }
}
return item
})
// Reverse items at current level
// use localeCompare since the reverse array method doesn't account for
// numeric strings
result.sort((a, b) => {
const aToCompare = a.id || a.href || a.value || ""
const bToCompare = b.id || b.href || b.value || ""
const comparison = aToCompare.localeCompare(bToCompare, undefined, {
numeric: true,
})
return comparison < 0 ? 1 : comparison > 0 ? -1 : 0
})
}
return result
}
module.exports = reverseSidebarItems