docs: redesign sidebar (#8408)

* initial changes

* redesign the sidebar + nav drawer

* changes to sidebar items

* finish up sidebar redesign

* support new sidebar in resources

* general fixes

* integrate in ui

* support api reference

* refactor

* integrate in user guide

* docs: fix build errors

* fix user guide build

* more refactoring

* added banner

* added bottom logo + icon

* fix up sidebar

* fix up paddings

* fix shadow bottom

* docs: add table of content (#8445)

* add toc types

* implement toc functionality

* finished toc redesign

* redesigned table of content

* mobile fixes

* truncate text in toc

* mobile fixes

* merge fixes

* implement redesign

* add hide sidebar

* add menu action item

* finish up hide sidebar design

* implement redesign in resources

* integrate in api reference

* integrate changes in ui

* fixes to api reference scrolling

* fix build error

* fix build errors

* fixes

* fixes to sidebar

* general fixes

* fix active category not closing

* fix long titles
This commit is contained in:
Shahed Nasser
2024-08-15 12:13:13 +03:00
committed by GitHub
parent 4cb28531e5
commit b4f3b8a79d
157 changed files with 5080 additions and 2010 deletions

View File

@@ -1,15 +1,65 @@
import React from "react"
import { Card, Link } from "../.."
"use client"
import React, { useEffect, useState } from "react"
import { Button, useIsBrowser } from "../.."
import { ExclamationCircleSolid, XMark } from "@medusajs/icons"
import clsx from "clsx"
const LOCAL_STORAGE_KEY = "banner-v2"
export type Bannerv2Props = {
className?: string
}
export const Bannerv2 = ({ className }: Bannerv2Props) => {
const [show, setShow] = useState(false)
const isBrowser = useIsBrowser()
useEffect(() => {
if (!isBrowser) {
return
}
const localStorageValue = localStorage.getItem(LOCAL_STORAGE_KEY)
if (!localStorageValue) {
setShow(true)
}
}, [isBrowser])
const handleClose = () => {
setShow(false)
localStorage.setItem(LOCAL_STORAGE_KEY, "true")
}
export const Bannerv2 = () => {
return (
<Card>
This documentation is for Medusa v2, which isn&apos;t ready for
production.
<br />
<br />
For production-use, refer to{" "}
<Link href="https://docs.medusajs.com">this documentation</Link> instead.
</Card>
<div
className={clsx(
"bg-medusa-bg-base hidden gap-docs_0.5 z-20",
"justify-between items-start rounded-docs_DEFAULT",
"p-docs_0.75 shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark",
show && "lg:flex",
className
)}
>
<span className="p-[2.5px]">
<ExclamationCircleSolid className="text-medusa-tag-orange-icon" />
</span>
<div className="flex flex-col gap-docs_0.125 flex-1">
<span className="text-compact-small-plus text-medusa-fg-base">
Medusa v2 and Docs under development
</span>
<span className="text-compact-small text-medusa-fg-subtle">
We are actively working on building and improving. Some sections may
be incomplete or subject to change. Thank you for your patience.
</span>
</div>
<Button
variant="transparent-clear"
className="text-medusa-fg-muted"
onClick={handleClose}
>
<XMark />
</Button>
</div>
)
}

View File

@@ -1,5 +1,4 @@
import React from "react"
import { Bordered } from "@/components/Bordered"
import clsx from "clsx"
import { IconProps } from "@medusajs/icons/dist/types"
@@ -15,37 +14,35 @@ export type BorderedIconProps = {
export const BorderedIcon = ({
icon = "",
IconComponent = null,
wrapperClassName,
iconWrapperClassName,
iconClassName,
iconColorClassName = "",
}: BorderedIconProps) => {
return (
<Bordered wrapperClassName={wrapperClassName}>
<span
className={clsx(
"rounded-docs_xs p-docs_0.125 bg-medusa-bg-component inline-flex items-center justify-center",
iconWrapperClassName
)}
>
{!IconComponent && (
<img
src={icon || ""}
className={clsx(iconClassName, "bordered-icon")}
alt=""
/>
)}
{IconComponent && (
<IconComponent
className={clsx(
"text-medusa-fg-subtle",
iconClassName,
"bordered-icon",
iconColorClassName
)}
/>
)}
</span>
</Bordered>
<span
className={clsx(
"rounded-docs_sm p-docs_0.125 bg-medusa-bg-base inline-flex items-center justify-center",
"shadow-border-base dark:shadow-border-base-dark",
iconWrapperClassName
)}
>
{!IconComponent && (
<img
src={icon || ""}
className={clsx(iconClassName, "bordered-icon")}
alt=""
/>
)}
{IconComponent && (
<IconComponent
className={clsx(
"text-medusa-fg-subtle rounded-docs_sm",
iconClassName,
"bordered-icon",
iconColorClassName
)}
/>
)}
</span>
)
}

View File

@@ -17,8 +17,13 @@ export const Breadcrumbs = () => {
tempBreadcrumbItems = getBreadcrumbsOfItem(item.previousSidebar)
}
const parentPath =
item.parentItem?.type === "link" ? item.parentItem.path : undefined
const firstItemPath =
item.default[0].type === "link" ? item.default[0].path : undefined
tempBreadcrumbItems.set(
item.parentItem?.path || item.top[0].path || "/",
parentPath || firstItemPath || "/",
item.parentItem?.childSidebarTitle || item.parentItem?.title || ""
)

View File

@@ -2,7 +2,8 @@
import React, { useMemo } from "react"
import { Card, CardList, MDXComponents, useSidebar } from "../.."
import { SidebarItemType } from "types"
import { InteractiveSidebarItem, SidebarItem, SidebarItemLink } from "types"
import slugify from "slugify"
type ChildDocsProps = {
onlyTopLevel?: boolean
@@ -30,16 +31,19 @@ export const ChildDocs = ({
: "all"
}, [showItems, hideItems])
const filterCondition = (item: SidebarItemType): boolean => {
const filterCondition = (item: SidebarItem): boolean => {
if (item.type === "separator") {
return false
}
switch (filterType) {
case "hide":
return (
(!item.path || !hideItems.includes(item.path)) &&
(item.type !== "link" || !hideItems.includes(item.path)) &&
!hideItems.includes(item.title)
)
case "show":
return (
(item.path !== undefined && showItems!.includes(item.path)) ||
(item.type === "link" && showItems!.includes(item.path)) ||
showItems!.includes(item.title)
)
case "all":
@@ -47,12 +51,16 @@ export const ChildDocs = ({
}
}
const filterItems = (items: SidebarItemType[]): SidebarItemType[] => {
const filterItems = (items: SidebarItem[]): SidebarItem[] => {
return items
.filter(filterCondition)
.map((item) => Object.assign({}, item))
.map((item) => {
if (item.children && filterType === "hide") {
if (
item.type !== "separator" &&
item.children &&
filterType === "hide"
) {
item.children = filterItems(item.children)
}
@@ -67,8 +75,7 @@ export const ChildDocs = ({
? Object.assign({}, currentItems)
: undefined
: {
top: [...(getActiveItem()?.children || [])],
bottom: [],
default: [...(getActiveItem()?.children || [])],
}
if (filterType === "all" || !targetItems) {
return targetItems
@@ -76,25 +83,34 @@ export const ChildDocs = ({
return {
...targetItems,
top: filterItems(targetItems.top),
bottom: filterItems(targetItems.bottom),
default: filterItems(targetItems.default),
}
}, [currentItems, type, getActiveItem, filterItems])
const filterNonInteractiveItems = (
items: SidebarItem[] | undefined
): InteractiveSidebarItem[] => {
return (
(items?.filter(
(item) => item.type !== "separator"
) as InteractiveSidebarItem[]) || []
)
}
const getChildrenForLevel = (
item: SidebarItemType,
item: InteractiveSidebarItem,
currentLevel = 1
): SidebarItemType[] | undefined => {
): InteractiveSidebarItem[] | undefined => {
if (currentLevel === childLevel) {
return item.children
return filterNonInteractiveItems(item.children)
}
if (!item.children) {
return
}
const childrenResult: SidebarItemType[] = []
const childrenResult: InteractiveSidebarItem[] = []
item.children.forEach((child) => {
filterNonInteractiveItems(item.children).forEach((child) => {
const childChildren = getChildrenForLevel(child, currentLevel + 1)
if (!childChildren) {
@@ -107,20 +123,34 @@ export const ChildDocs = ({
return childrenResult
}
const getTopLevelElms = (items?: SidebarItemType[]) => (
<CardList
items={
items?.map((childItem) => ({
title: childItem.title,
href: childItem.path,
showLinkIcon: false,
})) || []
}
/>
)
const getTopLevelElms = (items?: SidebarItem[]) => {
return (
<CardList
items={
filterNonInteractiveItems(items).map((childItem) => {
const href =
childItem.type === "link"
? childItem.path
: childItem.children?.length
? (
childItem.children.find(
(item) => item.type === "link"
) as SidebarItemLink
)?.path
: "#"
return {
title: childItem.title,
href,
showLinkIcon: false,
}
}) || []
}
/>
)
}
const getAllLevelsElms = (items?: SidebarItemType[]) =>
items?.map((item, key) => {
const getAllLevelsElms = (items?: SidebarItem[]) =>
filterNonInteractiveItems(items).map((item, key) => {
const itemChildren = getChildrenForLevel(item)
const HeadingComponent = itemChildren?.length
? MDXComponents["h2"]
@@ -130,12 +160,16 @@ export const ChildDocs = ({
<React.Fragment key={key}>
{HeadingComponent && (
<>
{!hideTitle && <HeadingComponent>{item.title}</HeadingComponent>}
{!hideTitle && (
<HeadingComponent id={slugify(item.title)}>
{item.title}
</HeadingComponent>
)}
<CardList
items={
itemChildren?.map((childItem) => ({
title: childItem.title,
href: childItem.path,
href: childItem.type === "link" ? childItem.path : "",
showLinkIcon: false,
})) || []
}
@@ -143,20 +177,19 @@ export const ChildDocs = ({
</>
)}
{!HeadingComponent && (
<Card title={item.title} href={item.path} showLinkIcon={false} />
<Card
title={item.title}
href={item.type === "link" ? item.path : ""}
showLinkIcon={false}
/>
)}
</React.Fragment>
)
})
const getElms = (items?: SidebarItemType[]) => {
const getElms = (items?: SidebarItem[]) => {
return onlyTopLevel ? getTopLevelElms(items) : getAllLevelsElms(items)
}
return (
<>
{getElms(filteredItems?.top)}
{getElms(filteredItems?.bottom)}
</>
)
return <>{getElms(filteredItems?.default)}</>
}

View File

@@ -1,39 +1,23 @@
"use client"
import React, { useCallback, useEffect, useRef, useState } from "react"
import React, { useEffect, useRef, useState } from "react"
import { Button } from "@/components"
import clsx from "clsx"
import { CSSTransition } from "react-transition-group"
import { HelpButtonActions } from "./Actions"
import { useIsBrowser } from "../.."
import { useClickOutside } from "../.."
export const HelpButton = () => {
const [showText, setShowText] = useState(false)
const [showHelp, setShowHelp] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const isBrowser = useIsBrowser()
const onClickOutside = useCallback(
(e: MouseEvent) => {
if (!ref.current?.contains(e.target as Node)) {
setShowHelp(false)
setShowText(false)
}
useClickOutside({
elmRef: ref,
onClickOutside: () => {
setShowHelp(false)
setShowText(false)
},
[ref.current]
)
useEffect(() => {
if (!isBrowser) {
return
}
window.document.addEventListener("click", onClickOutside)
return () => {
window.document.removeEventListener("click", onClickOutside)
}
}, [isBrowser])
})
useEffect(() => {
if (showHelp) {

View File

@@ -0,0 +1,23 @@
import React from "react"
import { IconProps } from "@medusajs/icons/dist/types"
export const HouseIcon = (props: IconProps) => {
return (
<svg
width="15"
height="16"
viewBox="0 0 15 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12.7044 5.5112L8.03778 1.96454C7.71956 1.72276 7.27956 1.72276 6.96222 1.96454L2.29556 5.5112C2.07422 5.6792 1.94444 5.94143 1.94444 6.21965V12.6676C1.94444 13.6499 2.74 14.4454 3.72222 14.4454H5.94444V10.8899C5.94444 10.3992 6.34267 10.001 6.83333 10.001H8.16667C8.65733 10.001 9.05556 10.3992 9.05556 10.8899V14.4454H11.2778C12.26 14.4454 13.0556 13.6499 13.0556 12.6676V6.21876C13.0556 5.94054 12.9258 5.68009 12.7044 5.5112Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownAdminIcon = (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}
>
<rect width="20" height="20" className="fill-medusa-tag-green-icon" />
<g clipPath="url(#clip0_9988_95571)">
<path
d="M13.25 4.5H6.75C5.233 4.5 4 5.733 4 7.25V12.75C4 14.267 5.233 15.5 6.75 15.5H13.25C14.767 15.5 16 14.267 16 12.75V7.25C16 5.733 14.767 4.5 13.25 4.5ZM6.5 8C5.948 8 5.5 7.552 5.5 7C5.5 6.448 5.948 6 6.5 6C7.052 6 7.5 6.448 7.5 7C7.5 7.552 7.052 8 6.5 8ZM9.5 8C8.948 8 8.5 7.552 8.5 7C8.5 6.448 8.948 6 9.5 6C10.052 6 10.5 6.448 10.5 7C10.5 7.552 10.052 8 9.5 8Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_9988_95571">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,41 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownDocIcon = (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}
>
<rect
width="20"
height="20"
className="fill-medusa-fg-base dark:fill-medusa-bg-base"
/>
<g clipPath="url(#clip0_9988_95547)">
<path
d="M14.25 16H7.25C6.009 16 5 14.991 5 13.75C5 13.336 5.336 13 5.75 13C6.164 13 6.5 13.336 6.5 13.75C6.5 14.164 6.836 14.5 7.25 14.5H14.25C14.664 14.5 15 14.836 15 15.25C15 15.664 14.664 16 14.25 16Z"
className="fill-medusa-fg-on-color"
/>
<path
d="M12.75 4H7.25C6.009 4 5 5.009 5 6.25V13.75C5 14.164 5.336 14.5 5.75 14.5C6.164 14.5 6.5 14.164 6.5 13.75C6.5 13.336 6.836 13 7.25 13H14.25C14.664 13 15 12.664 15 12.25V6.25C15 5.009 13.991 4 12.75 4ZM11.25 9H8.75C8.336 9 8 8.664 8 8.25C8 7.836 8.336 7.5 8.75 7.5H11.25C11.664 7.5 12 7.836 12 8.25C12 8.664 11.664 9 11.25 9Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_9988_95547">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,37 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownDocV1Icon = (props: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect width="20" height="20" className="fill-medusa-fg-subtle" />
<g clipPath="url(#clip0_10088_101562)">
<path
d="M8 4.02405C6.687 4.14505 5.646 5.18605 5.525 6.50005H4.75C4.336 6.50005 4 6.83605 4 7.25005C4 7.66405 4.336 8.00005 4.75 8.00005H5.5V9.25005H4.75C4.336 9.25005 4 9.58605 4 10C4 10.414 4.336 10.75 4.75 10.75H5.5V12H4.75C4.336 12 4 12.336 4 12.75C4 13.164 4.336 13.5 4.75 13.5H5.525C5.646 14.814 6.687 15.855 8 15.976V4.02405Z"
className="fill-medusa-fg-on-color"
/>
<path
d="M12.25 4H9.5V16H12.25C13.767 16 15 14.767 15 13.25V6.75C15 5.233 13.767 4 12.25 4Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_10088_101562">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,37 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownResourcesIcon = (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}
>
<rect width="20" height="20" className="fill-medusa-tag-orange-icon" />
<g clipPath="url(#clip0_9988_95555)">
<path
d="M15.25 12C14.836 12 14.5 11.664 14.5 11.25V8.75C14.5 8.061 13.939 7.5 13.25 7.5H10.614C10.386 7.5 10.172 7.397 10.029 7.219L9.425 6.467C9.187 6.17 8.831 6 8.45 6H6.749C6.06 6 5.499 6.561 5.499 7.25V11.25C5.499 11.664 5.163 12 4.749 12C4.335 12 3.999 11.664 3.999 11.25V7.25C4 5.733 5.233 4.5 6.75 4.5H8.451C9.289 4.5 10.07 4.875 10.596 5.528L10.974 6H13.25C14.767 6 16 7.233 16 8.75V11.25C16 11.664 15.664 12 15.25 12Z"
className="fill-medusa-fg-on-color"
/>
<path
d="M13.25 8.5H6.75C5.23122 8.5 4 9.73122 4 11.25V12.75C4 14.2688 5.23122 15.5 6.75 15.5H13.25C14.7688 15.5 16 14.2688 16 12.75V11.25C16 9.73122 14.7688 8.5 13.25 8.5Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_9988_95555">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,37 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownStoreIcon = (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}
>
<rect width="20" height="20" className="fill-medusa-tag-purple-icon" />
<g clipPath="url(#clip0_9988_95563)">
<path
d="M15.25 8.548C15.122 8.548 14.993 8.515 14.874 8.446L10 5.617L5.126 8.446C4.77 8.654 4.309 8.532 4.101 8.174C3.893 7.816 4.015 7.357 4.373 7.149L9.624 4.102C9.856 3.966 10.145 3.966 10.377 4.102L15.627 7.15C15.985 7.358 16.107 7.817 15.899 8.175C15.76 8.415 15.508 8.548 15.25 8.548Z"
className="fill-medusa-fg-on-color"
/>
<path
d="M10 7.35195L5 10.254V13.75C5 14.715 5.785 15.5 6.75 15.5H9.25V13.25C9.25 12.836 9.586 12.5 10 12.5C10.414 12.5 10.75 12.836 10.75 13.25V15.5H13.25C14.215 15.5 15 14.715 15 13.75V10.254L10 7.35195Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_9988_95563">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,37 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownUiIcon = (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}
>
<rect width="20" height="20" className="fill-medusa-tag-blue-icon" />
<g clipPath="url(#clip0_9988_95578)">
<path
d="M14.18 10.472L9.28899 8.685C8.82599 8.518 8.32399 8.627 7.97699 8.975C7.62799 9.323 7.51699 9.826 7.68599 10.288L9.47199 15.178C9.65599 15.679 10.114 16 10.645 16C10.654 16 10.664 16 10.672 16C11.215 15.989 11.672 15.648 11.836 15.132L12.394 13.394L14.131 12.838C14.648 12.673 14.988 12.216 15 11.673C15.011 11.131 14.689 10.658 14.18 10.472Z"
className="fill-medusa-fg-on-color"
/>
<path
d="M6.13499 11.894C6.05799 11.894 5.97999 11.882 5.90299 11.857C4.76499 11.487 4.00099 10.439 4.00099 9.25V7.75C3.99999 6.234 5.23299 5 6.74999 5H13.25C14.767 5 16 6.234 16 7.75V9.052C16 9.466 15.664 9.802 15.25 9.802C14.836 9.802 14.5 9.466 14.5 9.052V7.75C14.5 7.061 13.939 6.5 13.25 6.5H6.74999C6.06099 6.5 5.49999 7.061 5.49999 7.75V9.25C5.49999 9.788 5.84899 10.262 6.36699 10.43C6.76099 10.558 6.97599 10.981 6.84699 11.375C6.74399 11.692 6.45099 11.893 6.13399 11.893L6.13499 11.894Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_9988_95578">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,37 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const NavigationDropdownUserIcon = (props: IconProps) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect width="20" height="20" className="fill-medusa-tag-red-icon" />
<g clipPath="url(#clip0_10088_101526)">
<path
d="M10 8.99097C11.3807 8.99097 12.5 7.87168 12.5 6.49097C12.5 5.11025 11.3807 3.99097 10 3.99097C8.61929 3.99097 7.5 5.11025 7.5 6.49097C7.5 7.87168 8.61929 8.99097 10 8.99097Z"
className="fill-medusa-fg-on-color"
/>
<path
d="M14.533 12.639C13.601 11.011 11.864 10 10 10C8.136 10 6.398 11.011 5.467 12.639C5.218 13.073 5.162 13.593 5.313 14.067C5.463 14.539 5.809 14.93 6.26 15.139C7.501 15.713 8.75 16 10 16C11.25 16 12.499 15.713 13.74 15.139C14.191 14.93 14.536 14.539 14.687 14.067C14.838 13.593 14.782 13.073 14.533 12.64V12.639Z"
className="fill-medusa-fg-on-color"
/>
</g>
<defs>
<clipPath id="clip0_10088_101526">
<rect
width="12"
height="12"
className="fill-medusa-fg-on-color"
transform="translate(4 4)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -0,0 +1,42 @@
import { IconProps } from "@medusajs/icons/dist/types"
import React from "react"
export const SidebarLeftIcon = (props: IconProps) => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_10002_37978)">
<path
d="M12.6668 2.25H3.33344C2.3516 2.25 1.55566 3.0738 1.55566 4.09V11.91C1.55566 12.9262 2.3516 13.75 3.33344 13.75H12.6668C13.6486 13.75 14.4446 12.9262 14.4446 11.91V4.09C14.4446 3.0738 13.6486 2.25 12.6668 2.25Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.3999 5V11"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_10002_37978">
<rect
width="15"
height="15"
fill="none"
transform="translate(0.5 0.5)"
/>
</clipPath>
</defs>
</svg>
)
}

View File

@@ -9,9 +9,9 @@ export const Kbd = ({ children, className, ...props }: KbdProps) => {
className={clsx(
"rounded-docs_xs border-solid border border-medusa-border-base",
"inline-flex items-center justify-center",
"py-0 px-[6px]",
"py-0 px-docs_0.25",
"bg-medusa-bg-field",
"text-medusa-tag-neutral-text",
"text-medusa-fg-subtle",
"text-compact-x-small-plus font-base shadow-none",
className
)}

View File

@@ -0,0 +1,84 @@
"use client"
import React, { useMemo } from "react"
import { CurrentItemsState, useSidebar } from "../../.."
import clsx from "clsx"
import Link from "next/link"
import { SidebarItemLink } from "types"
export const MainNavBreadcrumbs = () => {
const { currentItems, getActiveItem } = useSidebar()
const getLinkPath = (item?: SidebarItemLink): string | undefined => {
if (!item) {
return
}
return item.isPathHref ? item.path : `#${item.path}`
}
const getBreadcrumbsOfItem = (
item: CurrentItemsState
): Map<string, string> => {
let tempBreadcrumbItems: Map<string, string> = new Map()
if (item.previousSidebar) {
tempBreadcrumbItems = getBreadcrumbsOfItem(item.previousSidebar)
}
const parentPath =
item.parentItem?.type === "link"
? getLinkPath(item.parentItem)
: undefined
const firstItemPath =
item.default[0].type === "link" ? getLinkPath(item.default[0]) : undefined
tempBreadcrumbItems.set(
parentPath || firstItemPath || "/",
item.parentItem?.childSidebarTitle || item.parentItem?.title || ""
)
return tempBreadcrumbItems
}
const breadcrumbItems = useMemo(() => {
const tempBreadcrumbItems: Map<string, string> = new Map()
if (currentItems) {
getBreadcrumbsOfItem(currentItems).forEach((value, key) =>
tempBreadcrumbItems.set(key, value)
)
}
const activeItem = getActiveItem()
if (activeItem) {
tempBreadcrumbItems.set(
getLinkPath(activeItem) || "/",
activeItem?.title || ""
)
}
return tempBreadcrumbItems
}, [currentItems, getActiveItem])
return (
<div
className={clsx(
"flex items-center",
"text-medusa-fg-muted text-compact-small"
)}
>
{Array.from(breadcrumbItems).map(([link, title]) => (
<React.Fragment key={link}>
<span>/</span>
<Link
href={link}
className={clsx(
"hover:text-medusa-fg-base transition-colors",
"px-docs_0.5 py-docs_0.25"
)}
>
{title}
</Link>
</React.Fragment>
))}
</div>
)
}

View File

@@ -0,0 +1,22 @@
"use client"
import React from "react"
import { useColorMode } from "../../../providers"
import { Button } from "../../.."
import { Moon, Sun } from "@medusajs/icons"
import clsx from "clsx"
export const MainNavColorMode = () => {
const { colorMode, toggleColorMode } = useColorMode()
return (
<Button
variant="transparent-clear"
className={clsx("!p-[6.5px] text-medusa-fg-muted")}
onClick={toggleColorMode}
>
{colorMode === "light" && <Sun />}
{colorMode === "dark" && <Moon />}
</Button>
)
}

View File

@@ -0,0 +1,13 @@
import clsx from "clsx"
import React from "react"
export const MainNavDivider = () => {
return (
<span
className={clsx(
"h-docs_0.75 w-px block bg-medusa-border-base",
"mx-docs_0.5"
)}
></span>
)
}

View File

@@ -0,0 +1,21 @@
import React from "react"
import { BorderedIcon } from "@/components"
import { IconProps } from "@medusajs/icons/dist/types"
export type MainNavigationDropdownIconProps = {
icon: React.FC<IconProps>
inDropdown?: boolean
}
export const MainNavigationDropdownIcon = ({
icon,
inDropdown = false,
}: MainNavigationDropdownIconProps) => {
return (
<BorderedIcon
IconComponent={icon}
iconClassName={inDropdown ? "w-docs_1 h-docs_1" : ""}
iconWrapperClassName="rounded-docs_xs"
/>
)
}

View File

@@ -0,0 +1,50 @@
"use client"
import React from "react"
import { NavigationDropdownItem } from "types"
import Link from "next/link"
import clsx from "clsx"
import { EllipseMiniSolid } from "@medusajs/icons"
import { MainNavigationDropdownIcon } from "../../Icon"
import { SidebarSeparator } from "../../../../Sidebar/Separator"
export type MainNavigationDropdownMenuItemProps = {
item: NavigationDropdownItem
onSelect: () => void
}
export const MainNavigationDropdownMenuItem = ({
item,
onSelect,
}: MainNavigationDropdownMenuItemProps) => {
switch (item.type) {
case "divider":
return <SidebarSeparator className="my-docs_0.25" />
case "link":
return (
<Link
href={item.path}
className={clsx(
"hover:bg-medusa-bg-component-hover",
"block rounded-docs_xs"
)}
prefetch={false}
onClick={onSelect}
>
<li
className={clsx(
"px-docs_0.5 py-docs_0.25",
"rounded-docs_xs text-medusa-fg-base",
"flex gap-docs_0.5 justify-start items-center"
)}
>
<span className={clsx(!item.isActive && "invisible")}>
<EllipseMiniSolid />
</span>
<MainNavigationDropdownIcon icon={item.icon} inDropdown={true} />
<span className="whitespace-nowrap">{item.title}</span>
</li>
</Link>
)
}
}

View File

@@ -0,0 +1,36 @@
"use client"
import clsx from "clsx"
import React from "react"
import { NavigationDropdownItem } from "types"
import { MainNavigationDropdownMenuItem } from "./Item"
export type MainNavigationDropdownMenuProps = {
items: NavigationDropdownItem[]
open: boolean
onSelect: () => void
}
export const MainNavigationDropdownMenu = ({
items,
open,
onSelect,
}: MainNavigationDropdownMenuProps) => {
return (
<ul
className={clsx(
"absolute top-[calc(100%+4px)] p-docs_0.25 z-50 lg:-left-docs_0.25",
"bg-medusa-bg-component rounded shadow-elevation-flyout",
!open && "hidden"
)}
>
{items.map((item, index) => (
<MainNavigationDropdownMenuItem
item={item}
key={index}
onSelect={onSelect}
/>
))}
</ul>
)
}

View File

@@ -0,0 +1,40 @@
"use client"
import React from "react"
import { NavigationDropdownItem } from "types"
import { TrianglesMini } from "@medusajs/icons"
import clsx from "clsx"
import { MainNavigationDropdownIcon } from "../Icon"
export type MainNavigationDropdownSelectedProps = {
item: NavigationDropdownItem
onClick: () => void
}
export const MainNavigationDropdownSelected = ({
item,
onClick,
}: MainNavigationDropdownSelectedProps) => {
if (item.type === "divider") {
return <></>
}
return (
<div
className={clsx(
"flex justify-between items-center",
"cursor-pointer rounded-docs_sm hover:bg-medusa-bg-hover"
)}
tabIndex={-1}
onClick={onClick}
>
<MainNavigationDropdownIcon icon={item.icon} />
<div className="flex gap-[6px] py-docs_0.25 px-docs_0.5 items-center">
<span className="text-medusa-fg-base whitespace-nowrap flex-1">
{item.title}
</span>
<TrianglesMini className="text-medusa-fg-muted" />
</div>
</div>
)
}

View File

@@ -0,0 +1,46 @@
"use client"
import clsx from "clsx"
import React, { useMemo, useRef, useState } from "react"
import { MainNavigationDropdownSelected } from "./Selected"
import { MainNavigationDropdownMenu } from "./Menu"
import { useClickOutside, useMainNav } from "../../.."
export const MainNavigationDropdown = () => {
const { navItems: items } = useMainNav()
const navigationRef = useRef<HTMLDivElement>(null)
const [menuOpen, setMenuOpen] = useState(false)
useClickOutside({
elmRef: navigationRef,
onClickOutside: () => {
setMenuOpen(false)
},
})
const selectedItem = useMemo(() => {
const activeItem = items.find(
(item) => item.type === "link" && item.isActive
)
if (!activeItem) {
return items.length ? items[0] : undefined
}
return activeItem
}, [items])
return (
<div className={clsx("relative z-50")} ref={navigationRef}>
{selectedItem && (
<MainNavigationDropdownSelected
item={selectedItem}
onClick={() => setMenuOpen((prev) => !prev)}
/>
)}
<MainNavigationDropdownMenu
items={items}
open={menuOpen}
onSelect={() => setMenuOpen(false)}
/>
</div>
)
}

View File

@@ -0,0 +1,24 @@
"use client"
import React from "react"
import { Button, useSidebar } from "../../.."
import clsx from "clsx"
import { SidebarLeft } from "@medusajs/icons"
export const MainNavSidebarOpener = () => {
const { desktopSidebarOpen, setDesktopSidebarOpen } = useSidebar()
if (desktopSidebarOpen) {
return <></>
}
return (
<Button
variant="transparent-clear"
className={clsx("!p-[6.5px] text-medusa-fg-muted", "mr-docs_0.5")}
onClick={() => setDesktopSidebarOpen(true)}
>
<SidebarLeft />
</Button>
)
}

View File

@@ -0,0 +1,38 @@
"use client"
import clsx from "clsx"
import React from "react"
import { MainNavigationDropdown } from "./NavigationDropdown"
import { MainNavBreadcrumbs } from "./Breadcrumb"
import { SearchModalOpener, useMainNav } from "../.."
import { MainNavColorMode } from "./ColorMode"
import Link from "next/link"
import { MainNavDivider } from "./Divider"
import { MainNavSidebarOpener } from "./SidebarOpener"
export const MainNav = () => {
const { reportIssueLink } = useMainNav()
return (
<div
className={clsx(
"hidden sm:flex justify-between items-center",
"px-docs_1 py-docs_0.75 w-full z-20",
"sticky top-0 bg-medusa-bg-base"
)}
>
<div className="flex items-center gap-docs_0.25">
<MainNavSidebarOpener />
<MainNavigationDropdown />
<MainNavBreadcrumbs />
</div>
<div className="flex items-center gap-docs_0.25">
<Link href={reportIssueLink} className="text-medusa-fg-muted">
Report Issue
</Link>
<MainNavDivider />
<MainNavColorMode />
<SearchModalOpener />
</div>
</div>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import clsx from "clsx"
import React from "react"
import { MenuItemAction } from "types"
export type MenuActionProps = {
item: MenuItemAction
}
export const MenuAction = ({ item }: MenuActionProps) => {
return (
<span
className={clsx(
"flex py-docs_0.25 px-docs_0.5",
"gap-docs_0.5 rounded-docs_xs",
"hover:bg-medusa-bg-component-hover",
"text-medusa-fg-base cursor-pointer"
)}
tabIndex={-1}
onClick={item.action}
>
<span className="text-medusa-fg-subtle">{item.icon}</span>
<span className="text-compact-small flex-1">{item.title}</span>
{item.shortcut && (
<span className="text-medusa-fg-subtle text-compact-small">
{item.shortcut}
</span>
)}
</span>
)
}

View File

@@ -0,0 +1,7 @@
"use client"
import React from "react"
export const MenuDivider = () => {
return <hr className="bg-medusa-border-menu-top mt-[3px] mb-[3px]" />
}

View File

@@ -0,0 +1,27 @@
"use client"
import clsx from "clsx"
import Link from "next/link"
import React from "react"
import { MenuItemLink } from "types"
export type MenuItemProps = {
item: MenuItemLink
}
export const MenuItem = ({ item }: MenuItemProps) => {
return (
<Link
className={clsx(
"flex py-docs_0.25 px-docs_0.5",
"gap-docs_0.5 rounded-docs_xs",
"hover:bg-medusa-bg-component-hover",
"text-medusa-fg-base"
)}
href={item.link}
>
<span className="text-medusa-fg-subtle">{item.icon}</span>
<span className="text-compact-small">{item.title}</span>
</Link>
)
}

View File

@@ -0,0 +1,31 @@
import clsx from "clsx"
import React from "react"
import { MenuItem as MenuItemType } from "types"
import { MenuItem } from "./Item"
import { MenuDivider } from "./Divider"
import { MenuAction } from "./Action"
export type MenuProps = {
items: MenuItemType[]
className?: string
}
export const Menu = ({ items, className }: MenuProps) => {
return (
<div
className={clsx(
"bg-medusa-bg-component p-docs_0.25 rounded-docs_DEFAULT",
"shadow-elevation-flyout dark:shadow-elevation-flyout-dark",
className
)}
>
{items.map((item, index) => (
<React.Fragment key={index}>
{item.type === "link" && <MenuItem item={item} />}
{item.type === "action" && <MenuAction item={item} />}
{item.type === "divider" && <MenuDivider />}
</React.Fragment>
))}
</div>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import clsx from "clsx"
import React from "react"
import { SidebarLeftIcon } from "../Icons/SidebarLeft"
import { Button, SearchModalOpener, useSidebar } from "../.."
import { MainNavigationDropdown } from "../MainNav/NavigationDropdown"
export const MobileNavigation = () => {
const { setMobileSidebarOpen } = useSidebar()
return (
<div
className={clsx(
"sm:hidden bg-medusa-bg-base",
"sticky top-0 w-full z-50 h-min",
"px-docs_0.75 py-docs_0.5",
"flex justify-between items-center",
"border-b border-medusa-border-base"
)}
>
<Button
variant="transparent-clear"
onClick={() => setMobileSidebarOpen(true)}
>
<SidebarLeftIcon />
</Button>
<MainNavigationDropdown />
<SearchModalOpener />
</div>
)
}

View File

@@ -88,7 +88,7 @@ export const Modal = ({
{...props}
className={clsx(
"fixed top-0 left-0 flex h-screen w-screen items-center justify-center",
"bg-medusa-bg-overlay",
"bg-medusa-bg-overlay z-50",
"hidden open:flex border-0 p-0",
className
)}

View File

@@ -1,23 +0,0 @@
"use client"
import React from "react"
import { NavbarIconButton, NavbarIconButtonProps } from "../IconButton"
import { useColorMode } from "@/providers"
import { Moon, Sun } from "@medusajs/icons"
export type NavbarColorModeToggleProps = {
buttonProps?: NavbarIconButtonProps
}
export const NavbarColorModeToggle = ({
buttonProps,
}: NavbarColorModeToggleProps) => {
const { colorMode, toggleColorMode } = useColorMode()
return (
<NavbarIconButton {...buttonProps} onClick={() => toggleColorMode()}>
{colorMode === "light" && <Sun className="text-medusa-fg-muted" />}
{colorMode === "dark" && <Moon className="text-medusa-fg-muted" />}
</NavbarIconButton>
)
}

View File

@@ -1,24 +0,0 @@
import React from "react"
import clsx from "clsx"
import { Button, ButtonProps } from "@/components"
export type NavbarIconButtonProps = ButtonProps
export const NavbarIconButton = ({
children,
className,
...props
}: NavbarIconButtonProps) => {
return (
<Button
className={clsx(
"[&>svg]:h-[22px] [&>svg]:w-[22px] btn-secondary-icon",
className
)}
variant="secondary"
{...props}
>
{children}
</Button>
)
}

View File

@@ -1,40 +0,0 @@
"use client"
import React from "react"
import clsx from "clsx"
import { Badge, BadgeProps, Link, LinkProps } from "@/components"
export type NavbarLinkProps = {
href: string
label: string
className?: string
activeValuePattern?: RegExp
isActive?: boolean
badge?: BadgeProps
} & LinkProps
export const NavbarLink = ({
href,
label,
className,
isActive,
badge,
}: NavbarLinkProps) => {
return (
<Link
href={href}
className={clsx(
isActive && "!text-medusa-fg-base",
!isActive && "!text-medusa-fg-subtle",
"text-compact-small-plus inline-block",
"hover:!text-medusa-fg-base",
className
)}
>
{label}
{badge && (
<Badge {...badge} className={clsx(badge.className, "ml-docs_0.5")} />
)}
</Link>
)
}

View File

@@ -1,35 +0,0 @@
"use client"
import React from "react"
import { useColorMode } from "@/providers"
import Link from "next/link"
import clsx from "clsx"
import Image from "next/image"
export type NavbarLogoProps = {
light: string
dark?: string
className?: string
imageClassName?: string
}
export const NavbarLogo = ({
light,
dark,
className,
imageClassName,
}: NavbarLogoProps) => {
const { colorMode } = useColorMode()
return (
<Link href={`/`} className={clsx("flex-1", className)}>
<Image
src={colorMode === "light" ? light : dark || light}
alt="Medusa Logo"
height={20}
width={20}
className={clsx("align-middle", imageClassName)}
/>
</Link>
)
}

View File

@@ -1,35 +0,0 @@
"use client"
import React from "react"
import { NavbarIconButton, NavbarIconButtonProps } from "../../IconButton"
import clsx from "clsx"
import { SidebarLeft, XMark } from "@medusajs/icons"
export type NavbarMobileMenuButtonProps = {
buttonProps?: NavbarIconButtonProps
mobileSidebarOpen: boolean
setMobileSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
isLoading?: boolean
}
export const NavbarMobileMenuButton = ({
buttonProps,
mobileSidebarOpen,
setMobileSidebarOpen,
isLoading = false,
}: NavbarMobileMenuButtonProps) => {
return (
<NavbarIconButton
{...buttonProps}
className={clsx("mr-docs_1 lg:!hidden", buttonProps?.className)}
onClick={() => {
if (!isLoading) {
setMobileSidebarOpen((prevValue) => !prevValue)
}
}}
>
{!mobileSidebarOpen && <SidebarLeft className="text-medusa-fg-muted" />}
{mobileSidebarOpen && <XMark className="text-medusa-fg-muted" />}
</NavbarIconButton>
)
}

View File

@@ -1,50 +0,0 @@
"use client"
import React from "react"
import { NavbarMobileMenuButton, NavbarMobileMenuButtonProps } from "./Button"
import { NavbarColorModeToggle } from "../ColorModeToggle"
import { NavbarSearchModalOpener } from "../SearchModalOpener"
import { useMobile } from "@/providers"
import clsx from "clsx"
import { NavbarLogo, NavbarLogoProps } from "../Logo"
export type NavbarMobileMenuProps = {
menuButton: NavbarMobileMenuButtonProps
logo: NavbarLogoProps
}
export const NavbarMobileMenu = ({
menuButton,
logo,
}: NavbarMobileMenuProps) => {
const { isMobile } = useMobile()
return (
<div className="flex w-full items-center justify-between lg:hidden">
{isMobile && (
<>
<NavbarMobileMenuButton
{...menuButton}
buttonProps={{
...(menuButton.buttonProps || {}),
variant: "transparent",
}}
/>
<NavbarLogo
{...logo}
className="lg:hidden"
imageClassName="mx-auto"
/>
<div className="flex">
<NavbarSearchModalOpener />
<NavbarColorModeToggle
buttonProps={{
variant: "transparent",
}}
/>
</div>
</>
)}
</div>
)
}

View File

@@ -1,10 +0,0 @@
import clsx from "clsx"
import React from "react"
export const NavbarDivider = () => {
return (
<div
className={clsx("w-px h-1/2 bg-medusa-fg-disabled", "mx-docs_0.5")}
></div>
)
}

View File

@@ -1,17 +0,0 @@
"use client"
import React from "react"
import { SearchModalOpener } from "@/components"
import { useMobile } from "@/providers"
export type NavbarSearchModalOpenerProps = {
isLoading?: boolean
}
export const NavbarSearchModalOpener = ({
isLoading,
}: NavbarSearchModalOpenerProps) => {
const { isMobile } = useMobile()
return <SearchModalOpener isMobile={isMobile} isLoading={isLoading} />
}

View File

@@ -1,86 +0,0 @@
import React from "react"
import clsx from "clsx"
import { NavbarLink, NavbarLinkProps } from "./Link"
import { NavbarColorModeToggle } from "./ColorModeToggle"
import { NavbarLogo, NavbarLogoProps } from "./Logo"
import { NavbarMobileMenu } from "./MobileMenu"
import { NavbarSearchModalOpener } from "./SearchModalOpener"
import { NavbarMobileMenuButtonProps } from "./MobileMenu/Button"
import { NavbarDivider } from "./NavbarDivider"
export type NavbarItem =
| {
type: "link"
props: NavbarLinkProps
}
| {
type: "divider"
props?: Record<string, unknown>
}
export type NavbarProps = {
logo: NavbarLogoProps
items: NavbarItem[]
showSearchOpener?: boolean
showColorModeToggle?: boolean
additionalActionsAfter?: React.ReactNode
additionalActionsBefore?: React.ReactNode
mobileMenuButton: NavbarMobileMenuButtonProps
isLoading?: boolean
className?: string
}
export const Navbar = ({
logo,
items,
showSearchOpener = true,
showColorModeToggle = true,
additionalActionsBefore,
additionalActionsAfter,
mobileMenuButton,
isLoading,
className,
}: NavbarProps) => {
return (
<nav
className={clsx(
"h-navbar w-full justify-between",
"bg-docs-bg dark:bg-docs-bg-dark border-medusa-border-base border-b",
className
)}
>
<div
className={clsx(
"h-navbar max-w-xxl py-docs_0.75 mx-auto flex w-full justify-between px-docs_1 lg:px-docs_3"
)}
>
<div className="hidden w-full items-center gap-docs_0.5 lg:flex lg:w-auto lg:gap-docs_1.5">
<NavbarLogo {...logo} />
{items.map(({ type, props }, index) => {
switch (type) {
case "divider":
return <NavbarDivider key={index} />
default:
return <NavbarLink key={index} {...props} />
}
})}
</div>
<div className="hidden min-w-0 flex-1 items-center justify-end gap-docs_0.5 lg:flex">
{additionalActionsBefore}
{showSearchOpener && (
<NavbarSearchModalOpener isLoading={isLoading} />
)}
{showColorModeToggle && <NavbarColorModeToggle />}
{additionalActionsAfter}
</div>
<NavbarMobileMenu
logo={logo}
menuButton={{
...mobileMenuButton,
isLoading,
}}
/>
</div>
</nav>
)
}

View File

@@ -26,7 +26,7 @@ export const NotificationContainer = () => {
return (
<TransitionGroup
className={clsx(
"flex fixed flex-col gap-docs_0.5 right-0",
"flex fixed z-40 flex-col gap-docs_0.5 right-0",
"md:w-auto w-full overflow-y-auto",
"max-h-[50%] md:max-h-[calc(100vh-57px)]",
"max-[768px]:max-h-[50%]",

View File

@@ -2,20 +2,21 @@
import React, { MouseEvent, useMemo } from "react"
import clsx from "clsx"
import { useSearch } from "@/providers"
import { Button, InputText, Kbd } from "@/components"
import { useMobile, useSearch } from "@/providers"
import { Button } from "@/components"
import { MagnifyingGlass } from "@medusajs/icons"
import { useKeyboardShortcut } from "@/hooks"
export type SearchModalOpenerProps = {
isLoading?: boolean
isMobile?: boolean
className?: string
}
export const SearchModalOpener = ({
isLoading = false,
isMobile = false,
className,
}: SearchModalOpenerProps) => {
const { isMobile } = useMobile()
const { setIsOpen } = useSearch()
const isApple = useMemo(() => {
return typeof navigator !== "undefined"
@@ -52,39 +53,19 @@ export const SearchModalOpener = ({
</Button>
)}
{!isMobile && (
<div
className={clsx("relative w-min hover:cursor-pointer group")}
<Button
className={clsx(
"relative hover:cursor-pointer group",
"flex gap-[6px] !py-docs_0.25 !px-docs_0.5",
"justify-between items-center text-medusa-fg-muted",
className
)}
variant="transparent-clear"
onClick={handleOpen}
>
<MagnifyingGlass
className={clsx(
"absolute left-docs_0.5 top-[5px]",
"text-medusa-fg-muted"
)}
/>
<InputText
type="search"
className={clsx(
"placeholder:text-compact-small",
"!py-[5px] !pl-[36px] !pr-docs_0.5",
"cursor-pointer select-none"
)}
placeholder="Find something"
onClick={handleOpen}
onFocus={(e) => e.target.blur()}
tabIndex={-1}
addGroupStyling={true}
/>
<span
className={clsx(
"gap-docs_0.25 flex",
"absolute right-docs_0.5 top-[5px]"
)}
>
<Kbd>{isApple ? "⌘" : "Ctrl"}</Kbd>
<Kbd>K</Kbd>
</span>
</div>
<MagnifyingGlass />
<span>{isApple ? "⌘" : "Ctrl"}K</span>
</Button>
)}
</>
)

View File

@@ -1,26 +0,0 @@
"use client"
import React from "react"
import { useSidebar } from "../../../providers"
import clsx from "clsx"
import { ArrowUturnLeft } from "@medusajs/icons"
export const SidebarBack = () => {
const { goBack } = useSidebar()
return (
<div
className={clsx(
"mb-docs_1.5 cursor-pointer",
"flex items-center gap-docs_0.5 rounded-docs_sm px-docs_0.5 py-[6px] hover:no-underline",
"border border-transparent",
"text-medusa-fg-subtle text-medium-plus"
)}
tabIndex={-1}
onClick={goBack}
>
<ArrowUturnLeft className="mr-docs_0.5" />
<span>Back</span>
</div>
)
}

View File

@@ -0,0 +1,34 @@
"use client"
import React from "react"
import clsx from "clsx"
import { InteractiveSidebarItem } from "types"
import { ArrowUturnLeft } from "@medusajs/icons"
import { useSidebar } from "../../.."
type SidebarTitleProps = {
item: InteractiveSidebarItem
}
export const SidebarChild = ({ item }: SidebarTitleProps) => {
const { goBack } = useSidebar()
return (
<div className="px-docs_0.75">
<div
onClick={goBack}
className={clsx(
"flex items-center justify-start my-docs_0.75 gap-[10px]",
"border border-transparent cursor-pointer mx-docs_0.5",
"!text-medusa-fg-base !text-compact-small-plus"
)}
tabIndex={-1}
>
<ArrowUturnLeft />
<span className="truncate flex-1">
{item.childSidebarTitle || item.title}
</span>
</div>
</div>
)
}

View File

@@ -0,0 +1,115 @@
"use client"
// @refresh reset
import React, { useEffect, useMemo, useRef, useState } from "react"
import { SidebarItemCategory as SidebarItemCategoryType } from "types"
import { Loading, SidebarItem, useSidebar } from "../../../.."
import clsx from "clsx"
import { MinusMini, PlusMini } from "@medusajs/icons"
export type SidebarItemCategory = {
item: SidebarItemCategoryType
expandItems?: boolean
} & React.AllHTMLAttributes<HTMLDivElement>
export const SidebarItemCategory = ({
item,
expandItems = true,
className,
}: SidebarItemCategory) => {
const [showLoading, setShowLoading] = useState(false)
const [open, setOpen] = useState(
item.initialOpen !== undefined ? item.initialOpen : expandItems
)
const childrenRef = useRef<HTMLUListElement>(null)
const { isChildrenActive } = useSidebar()
useEffect(() => {
if (open && !item.loaded) {
setShowLoading(true)
}
}, [open])
useEffect(() => {
if (item.loaded && showLoading) {
setShowLoading(false)
}
}, [item])
useEffect(() => {
const isActive = isChildrenActive(item)
if (isActive && !open) {
setOpen(true)
}
}, [isChildrenActive])
const handleOpen = () => {
item.onOpen?.()
}
const isTitleOneWord = useMemo(
() => item.title.split(" ").length === 1,
[item.title]
)
return (
<div className={clsx("my-docs_0.75 w-full relative", className)}>
<div className="px-docs_0.75">
<div
className={clsx(
"py-docs_0.25 px-docs_0.5",
"flex justify-between items-center gap-docs_0.5",
"text-medusa-fg-muted",
"cursor-pointer relative",
"z-[2]",
!isTitleOneWord && "break-words"
)}
tabIndex={-1}
onClick={() => {
if (!open) {
handleOpen()
}
setOpen((prev) => !prev)
}}
>
<span
className={clsx(
"text-compact-x-small-plus",
isTitleOneWord && "truncate"
)}
>
{item.title}
</span>
{item.additionalElms}
{!item.additionalElms && (
<>
{open && <MinusMini />}
{!open && <PlusMini />}
</>
)}
</div>
</div>
<ul
className={clsx(
"ease-ease",
"flex flex-col gap-docs_0.125",
"z-[1] relative",
!open && "overflow-hidden m-0 h-0"
)}
ref={childrenRef}
>
{showLoading && (
<Loading
count={3}
className="!mb-0 !px-docs_0.5"
barClassName="h-[20px]"
/>
)}
{item.children?.map((childItem, index) => (
<SidebarItem item={childItem} key={index} expandItems={expandItems} />
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,147 @@
"use client"
// @refresh reset
import React, { useCallback, useEffect, useMemo, useRef } from "react"
import { SidebarItemLink as SidebarItemlinkType } from "types"
import {
checkSidebarItemVisibility,
SidebarItem,
useMobile,
useSidebar,
} from "../../../.."
import clsx from "clsx"
import Link from "next/link"
export type SidebarItemLinkProps = {
item: SidebarItemlinkType
nested?: boolean
} & React.AllHTMLAttributes<HTMLLIElement>
export const SidebarItemLink = ({
item,
className,
nested = false,
}: SidebarItemLinkProps) => {
const {
isLinkActive,
setMobileSidebarOpen: setSidebarOpen,
disableActiveTransition,
sidebarRef,
sidebarTopHeight,
} = useSidebar()
const { isMobile } = useMobile()
const active = useMemo(() => isLinkActive(item, true), [isLinkActive, item])
const ref = useRef<HTMLLIElement>(null)
const newTopCalculator = useCallback(() => {
if (!sidebarRef.current || !ref.current) {
return 0
}
const sidebarBoundingRect = sidebarRef.current.getBoundingClientRect()
const itemBoundingRect = ref.current.getBoundingClientRect()
return (
itemBoundingRect.top -
(sidebarBoundingRect.top + sidebarTopHeight) +
sidebarRef.current.scrollTop
)
}, [sidebarTopHeight, sidebarRef, ref])
useEffect(() => {
if (active && ref.current && sidebarRef.current && !isMobile) {
const isVisible = checkSidebarItemVisibility(
(ref.current.children.item(0) as HTMLElement) || ref.current,
!disableActiveTransition
)
if (isVisible) {
return
}
if (!disableActiveTransition) {
ref.current.scrollIntoView({
block: "center",
})
} else {
sidebarRef.current.scrollTo({
top: newTopCalculator(),
})
}
}
}, [
active,
sidebarRef.current,
disableActiveTransition,
isMobile,
newTopCalculator,
])
const hasChildren = useMemo(() => {
return !item.isChildSidebar && (item.children?.length || 0) > 0
}, [item.children])
const isTitleOneWord = useMemo(
() => item.title.split(" ").length === 1,
[item.title]
)
return (
<li ref={ref}>
<span className="block px-docs_0.75">
<Link
href={item.isPathHref ? item.path : `#${item.path}`}
className={clsx(
"py-docs_0.25 px-docs_0.5",
"block w-full rounded-docs_sm",
!isTitleOneWord && "break-words",
active && [
"bg-medusa-bg-base",
"shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark",
"text-medusa-fg-base",
],
!active && [
!nested && "text-medusa-fg-subtle",
nested && "text-medusa-fg-muted",
"hover:bg-medusa-bg-base-hover lg:hover:bg-medusa-bg-subtle-hover",
],
"text-compact-small-plus",
"flex justify-between items-center gap-[6px]",
className
)}
scroll={true}
onClick={() => {
if (isMobile) {
setSidebarOpen(false)
}
}}
replace={!item.isPathHref}
shallow={!item.isPathHref}
{...item.linkProps}
>
<span className={clsx(isTitleOneWord && "truncate")}>
{item.title}
</span>
{item.additionalElms}
</Link>
</span>
{hasChildren && (
<ul
className={clsx(
"ease-ease overflow-hidden",
"flex flex-col gap-docs_0.125",
!item.childrenSameLevel && "pl-docs_1.5",
"pt-docs_0.125 pb-docs_0.5"
)}
>
{item.children!.map((childItem, index) => (
<SidebarItem
item={childItem}
key={index}
nested={!item.childrenSameLevel}
/>
))}
</ul>
)}
</li>
)
}

View File

@@ -0,0 +1,145 @@
"use client"
// @refresh reset
import React, { useEffect, useMemo, useRef } from "react"
import { SidebarItemSubCategory as SidebarItemSubCategoryType } from "types"
import {
checkSidebarItemVisibility,
SidebarItem,
useMobile,
useSidebar,
} from "../../../.."
import clsx from "clsx"
export type SidebarItemLinkProps = {
item: SidebarItemSubCategoryType
nested?: boolean
} & React.AllHTMLAttributes<HTMLLIElement>
export const SidebarItemSubCategory = ({
item,
className,
nested = false,
}: SidebarItemLinkProps) => {
const { isLinkActive, disableActiveTransition, sidebarRef } = useSidebar()
const { isMobile } = useMobile()
const active = useMemo(
() => !isMobile && isLinkActive(item, true),
[isLinkActive, item, isMobile]
)
const ref = useRef<HTMLLIElement>(null)
/**
* Tries to place the item in the center of the sidebar
*/
const newTopCalculator = (): number => {
if (!sidebarRef.current || !ref.current) {
return 0
}
const sidebarBoundingRect = sidebarRef.current.getBoundingClientRect()
const sidebarHalf = sidebarBoundingRect.height / 2
const itemTop = ref.current.offsetTop
const itemBottom =
itemTop + (ref.current.children.item(0) as HTMLElement)?.clientHeight
// try deducting half
let newTop = itemTop - sidebarHalf
let newBottom = newTop + sidebarBoundingRect.height
if (newTop <= itemTop && newBottom >= itemBottom) {
return newTop
}
// try adding half
newTop = itemTop + sidebarHalf
newBottom = newTop + sidebarBoundingRect.height
if (newTop <= itemTop && newBottom >= itemBottom) {
return newTop
}
//return the item's top minus some top margin
return itemTop - sidebarBoundingRect.top
}
useEffect(() => {
if (
active &&
ref.current &&
sidebarRef.current &&
window.innerWidth >= 1025
) {
if (
!disableActiveTransition &&
!checkSidebarItemVisibility(
(ref.current.children.item(0) as HTMLElement) || ref.current,
!disableActiveTransition
)
) {
ref.current.scrollIntoView({
block: "center",
})
} else if (disableActiveTransition) {
sidebarRef.current.scrollTo({
top: newTopCalculator(),
})
}
}
}, [active, sidebarRef.current, disableActiveTransition])
const hasChildren = useMemo(() => {
return item.children?.length || 0 > 0
}, [item.children])
const isTitleOneWord = useMemo(
() => item.title.split(" ").length === 1,
[item.title]
)
return (
<li ref={ref}>
<span className="block px-docs_0.75">
<span
className={clsx(
"py-docs_0.25 px-docs_0.5",
"block w-full",
!isTitleOneWord && "break-words",
active && [
"rounded-docs_sm",
"shadow-borders-base dark:shadow-borders-base-dark",
"text-medusa-fg-base",
],
!active && [
!nested && "text-medusa-fg-subtle",
nested && "text-medusa-fg-muted",
],
"text-compact-small-plus",
className
)}
>
<span className={clsx(isTitleOneWord && "truncate")}>
{item.title}
</span>
{item.additionalElms}
</span>
</span>
{hasChildren && (
<ul
className={clsx(
"ease-ease overflow-hidden",
"flex flex-col gap-docs_0.125",
!item.childrenSameLevel && "pl-docs_1.5",
"pb-docs_0.5 pt-docs_0.125"
)}
>
{item.children!.map((childItem, index) => (
<SidebarItem
item={childItem}
key={index}
nested={!item.childrenSameLevel}
/>
))}
</ul>
)}
</li>
)
}

View File

@@ -1,204 +1,37 @@
"use client"
// @refresh reset
import React, { useEffect, useMemo, useRef, useState } from "react"
import { useSidebar } from "@/providers"
import clsx from "clsx"
import Link from "next/link"
import { checkSidebarItemVisibility } from "@/utils"
import { Loading } from "@/components"
import { SidebarItemType } from "types"
import React from "react"
import { SidebarItem as SidebarItemType } from "types"
import { SidebarItemCategory } from "./Category"
import { SidebarItemLink } from "./Link"
import { SidebarItemSubCategory } from "./SubCategory"
import { SidebarSeparator } from "../Separator"
export type SidebarItemProps = {
item: SidebarItemType
nested?: boolean
expandItems?: boolean
currentLevel?: number
isSidebarTitle?: boolean
sidebarHasParent?: boolean
isMobile?: boolean
} & React.AllHTMLAttributes<HTMLLIElement>
hasNextItems?: boolean
} & React.AllHTMLAttributes<HTMLElement>
export const SidebarItem = ({
item,
nested = false,
expandItems = false,
className,
currentLevel = 1,
sidebarHasParent = false,
isMobile = false,
hasNextItems = false,
...props
}: SidebarItemProps) => {
const [showLoading, setShowLoading] = useState(false)
const {
isItemActive,
setMobileSidebarOpen: setSidebarOpen,
disableActiveTransition,
noTitleStyling,
sidebarRef,
} = useSidebar()
const active = useMemo(
() => !isMobile && isItemActive(item, nested),
[isItemActive, item, nested, isMobile]
)
const collapsed = !expandItems && !isItemActive(item, true)
const ref = useRef<HTMLLIElement>(null)
const itemChildren = useMemo(() => {
return item.isChildSidebar ? undefined : item.children
}, [item])
const canHaveTitleStyling = useMemo(
() =>
item.hasTitleStyling ||
((itemChildren?.length || !item.loaded) && !noTitleStyling && !nested),
[itemChildren, noTitleStyling, item, nested]
)
const classNames = useMemo(
() =>
clsx(
"flex items-center justify-between gap-docs_0.5 rounded-docs_xs px-docs_0.5 py-[6px]",
"hover:no-underline hover:bg-medusa-bg-component-hover",
!canHaveTitleStyling && "text-compact-small-plus text-medusa-fg-subtle",
canHaveTitleStyling &&
"text-compact-x-small-plus text-medusa-fg-muted uppercase",
!item.path && "cursor-default",
item.path !== undefined &&
active &&
"text-medusa-fg-base bg-medusa-bg-component-hover"
),
[canHaveTitleStyling, active, item.path]
)
/**
* Tries to place the item in the center of the sidebar
*/
const newTopCalculator = (): number => {
if (!sidebarRef.current || !ref.current) {
return 0
}
const sidebarBoundingRect = sidebarRef.current.getBoundingClientRect()
const sidebarHalf = sidebarBoundingRect.height / 2
const itemTop = ref.current.offsetTop
const itemBottom =
itemTop + (ref.current.children.item(0) as HTMLElement)?.clientHeight
// try deducting half
let newTop = itemTop - sidebarHalf
let newBottom = newTop + sidebarBoundingRect.height
if (newTop <= itemTop && newBottom >= itemBottom) {
return newTop
}
// try adding half
newTop = itemTop + sidebarHalf
newBottom = newTop + sidebarBoundingRect.height
if (newTop <= itemTop && newBottom >= itemBottom) {
return newTop
}
//return the item's top minus some top margin
return itemTop - sidebarBoundingRect.top
switch (item.type) {
case "category":
return (
<>
<SidebarItemCategory item={item} {...props} />
{hasNextItems && <SidebarSeparator />}
</>
)
case "sub-category":
return <SidebarItemSubCategory item={item} {...props} />
case "link":
return <SidebarItemLink item={item} {...props} />
case "separator":
return <SidebarSeparator />
}
useEffect(() => {
if (
active &&
ref.current &&
sidebarRef.current &&
window.innerWidth >= 1025
) {
if (
!disableActiveTransition &&
!checkSidebarItemVisibility(
(ref.current.children.item(0) as HTMLElement) || ref.current,
!disableActiveTransition
)
) {
ref.current.scrollIntoView({
block: "center",
})
} else if (disableActiveTransition) {
sidebarRef.current.scrollTo({
top: newTopCalculator(),
})
}
}
if (active) {
setShowLoading(true)
}
}, [active, sidebarRef.current, disableActiveTransition])
return (
<li
className={clsx(
canHaveTitleStyling &&
!collapsed && [
!sidebarHasParent && "my-docs_1.5",
sidebarHasParent && "[&:not(:first-child)]:my-docs_1.5",
],
!canHaveTitleStyling &&
!nested &&
active &&
!disableActiveTransition &&
"mt-docs_1.5",
!expandItems &&
((canHaveTitleStyling && !collapsed) ||
(!canHaveTitleStyling && !nested && active)) &&
"-translate-y-docs_1 transition-transform",
className
)}
ref={ref}
>
{item.path === undefined && (
<span className={classNames}>
<span>{item.title}</span>
{item.additionalElms}
</span>
)}
{item.path !== undefined && (
<Link
href={item.isPathHref ? item.path : `#${item.path}`}
className={classNames}
scroll={true}
onClick={() => {
if (window.innerWidth < 1025) {
setSidebarOpen(false)
}
}}
replace={!item.isPathHref}
shallow={!item.isPathHref}
{...item.linkProps}
>
<span>{item.title}</span>
{item.additionalElms}
</Link>
)}
{itemChildren && (
<ul
className={clsx("ease-ease overflow-hidden", collapsed && "m-0 h-0")}
style={{
paddingLeft: noTitleStyling ? `${currentLevel * 6}px` : 0,
}}
>
{showLoading && !item.loaded && (
<Loading
count={3}
className="!mb-0 !px-docs_0.5"
barClassName="h-[20px]"
/>
)}
{itemChildren?.map((childItem, index) => (
<SidebarItem
item={childItem}
key={index}
nested={true}
currentLevel={currentLevel + 1}
expandItems={expandItems}
/>
))}
</ul>
)}
</li>
)
}

View File

@@ -0,0 +1,22 @@
"use client"
import clsx from "clsx"
import React from "react"
export type SidebarSeparatorProps = {
className?: string
}
export const SidebarSeparator = ({ className }: SidebarSeparatorProps) => {
return (
<div className="px-docs_0.75">
<span
className={clsx(
"block w-full h-px relative my-docs_0.75 bg-border-dotted",
"bg-[length:4px_1px] bg-repeat-x bg-bottom",
className
)}
></span>
</div>
)
}

View File

@@ -1,27 +0,0 @@
import React from "react"
import Link from "next/link"
import clsx from "clsx"
import { SidebarItemType } from "types"
type SidebarTitleProps = {
item: SidebarItemType
}
export const SidebarTitle = ({ item }: SidebarTitleProps) => {
return (
<Link
className={clsx(
"flex items-center justify-between gap-docs_0.5 rounded-docs_sm px-docs_0.5 pb-[6px] hover:no-underline",
"border border-transparent",
"text-medusa-fg-subtle text-large-plus"
)}
href={item.isPathHref && item.path ? item.path : `#${item.path}`}
replace={!item.isPathHref}
shallow={!item.isPathHref}
{...item.linkProps}
>
<span>{item.childSidebarTitle || item.title}</span>
{item.additionalElms}
</Link>
)
}

View File

@@ -0,0 +1,90 @@
"use client"
import clsx from "clsx"
import React, { useRef, useState } from "react"
import {
BorderedIcon,
getOsShortcut,
useClickOutside,
useSidebar,
} from "../../../.."
import {
EllipsisHorizontal,
SidebarLeft,
TimelineVertical,
} from "@medusajs/icons"
import { MedusaIcon } from "../../../Icons/MedusaLogo"
import { HouseIcon } from "../../../Icons/House"
import { Menu } from "../../../Menu"
export const SidebarTopMedusaMenu = () => {
const [openMenu, setOpenMenu] = useState(false)
const { setDesktopSidebarOpen } = useSidebar()
const ref = useRef<HTMLDivElement>(null)
const toggleOpen = () => setOpenMenu((prev) => !prev)
useClickOutside({
elmRef: ref,
onClickOutside: () => {
setOpenMenu(false)
},
})
return (
<div className={clsx("p-docs_0.75", "relative")} ref={ref}>
<div
className={clsx(
"flex justify-between items-center gap-docs_0.5",
"rounded-docs_sm hover:bg-medusa-bg-subtle-hover cursor-pointer",
"py-docs_0.125 pl-docs_0.125 pr-docs_0.5"
)}
tabIndex={-1}
onClick={toggleOpen}
>
<BorderedIcon IconComponent={MedusaIcon} />
<span className="text-compact-small-plus text-medusa-fg-base flex-1">
Medusa Docs
</span>
<EllipsisHorizontal className="text-medusa-fg-subtle" />
</div>
<div
className={clsx(
"absolute w-[calc(100%-16px)] bottom-[calc(-100%-40px)]",
"left-docs_0.5 z-40",
!openMenu && "hidden"
)}
>
<Menu
items={[
{
type: "link",
icon: <HouseIcon />,
title: "Homepage",
link: "https://medusajs.com",
},
{
type: "link",
icon: <TimelineVertical />,
title: "Changelog",
link: "https://medusajs.com/changelog",
},
{
type: "divider",
},
{
type: "action",
title: "Hide Sidebar",
icon: <SidebarLeft />,
shortcut: `${getOsShortcut()}.`,
action: () => {
setDesktopSidebarOpen(false)
setOpenMenu(false)
},
},
]}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,21 @@
"use client"
import React from "react"
import { Button, useSidebar } from "../../../.."
import { XMarkMini } from "@medusajs/icons"
export const SidebarTopMobileClose = () => {
const { setMobileSidebarOpen } = useSidebar()
return (
<div className="m-docs_0.75 lg:hidden">
<Button
variant="transparent-clear"
onClick={() => setMobileSidebarOpen(false)}
className="!p-0 hover:!bg-transparent"
>
<XMarkMini className="text-medusa-fg-subtle" />
</Button>
</div>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import React from "react"
import { SidebarChild } from "../Child"
import { InteractiveSidebarItem } from "types"
import { SidebarSeparator } from "../Separator"
import { SidebarTopMobileClose } from "./MobileClose"
import { SidebarTopMedusaMenu } from "./MedusaMenu"
export type SidebarTopProps = {
parentItem?: InteractiveSidebarItem
}
export const SidebarTop = React.forwardRef<HTMLDivElement, SidebarTopProps>(
function SidebarTop({ parentItem }, ref) {
return (
<div className="pt-docs_0.25">
<SidebarTopMobileClose />
<div ref={ref}>
<SidebarTopMedusaMenu />
{parentItem && (
<>
<SidebarSeparator />
<SidebarChild item={parentItem} />
</>
)}
<SidebarSeparator className="!my-0" />
</div>
</div>
)
}
)

View File

@@ -5,125 +5,153 @@ import { useSidebar } from "@/providers"
import clsx from "clsx"
import { Loading } from "@/components"
import { SidebarItem } from "./Item"
import { SidebarTitle } from "./Title"
import { SidebarBack } from "./Back"
import { CSSTransition, SwitchTransition } from "react-transition-group"
import { SidebarTop, SidebarTopProps } from "./Top"
import useResizeObserver from "@react-hook/resize-observer"
import { SidebarSeparator } from "./Separator"
import { useClickOutside, useKeyboardShortcut } from "@/hooks"
export type SidebarProps = {
className?: string
expandItems?: boolean
banner?: React.ReactNode
sidebarTopProps?: Omit<SidebarTopProps, "parentItem">
}
export const Sidebar = ({
className = "",
expandItems = false,
banner,
expandItems = true,
sidebarTopProps,
}: SidebarProps) => {
const sidebarWrapperRef = useRef(null)
const sidebarTopRef = useRef<HTMLDivElement>(null)
const {
items,
currentItems,
mobileSidebarOpen,
desktopSidebarOpen,
setMobileSidebarOpen,
staticSidebarItems,
sidebarRef,
sidebarTopHeight,
setSidebarTopHeight,
desktopSidebarOpen,
setDesktopSidebarOpen,
} = useSidebar()
useClickOutside({
elmRef: sidebarWrapperRef,
onClickOutside: () => {
if (mobileSidebarOpen) {
setMobileSidebarOpen(false)
}
},
})
useKeyboardShortcut({
metakey: true,
shortcutKeys: ["."],
action: () => {
setDesktopSidebarOpen((prev) => !prev)
},
})
const sidebarItems = useMemo(
() => currentItems || items,
[items, currentItems]
)
const sidebarHasParent = useMemo(
() => sidebarItems.parentItem !== undefined,
[sidebarItems]
)
useResizeObserver(sidebarTopRef, () => {
setSidebarTopHeight(sidebarTopRef.current?.clientHeight || 0)
})
return (
<aside
className={clsx(
"clip bg-docs-bg dark:bg-docs-bg-dark block",
"border-medusa-border-base border-0 border-r border-solid",
"fixed -left-full top-0 h-screen transition-[left] lg:relative lg:left-0 lg:top-auto lg:h-auto",
"lg:w-sidebar w-full",
mobileSidebarOpen && "!left-0 z-50 top-[57px]",
!desktopSidebarOpen && "!absolute !-left-full",
className
<>
{mobileSidebarOpen && (
<div
className={clsx(
"lg:hidden bg-medusa-bg-overlay opacity-70",
"fixed top-0 left-0 w-full h-full z-10"
)}
></div>
)}
style={{
animationFillMode: "forwards",
}}
>
<ul
<aside
className={clsx(
"sticky top-0 h-screen max-h-screen w-full list-none p-0",
"px-docs_1.5 pb-[57px] pt-docs_1.5",
"flex flex-col"
"bg-medusa-bg-base lg:bg-transparent block",
"fixed -left-full top-0 h-[calc(100%-16px)] transition-[left] lg:relative lg:h-auto",
"max-w-sidebar-xs sm:max-w-sidebar-sm md:max-w-sidebar-md lg:max-w-sidebar-lg",
"xl:max-w-sidebar-xl xxl:max-w-sidebar-xxl xxxl:max-w-sidebar-xxxl",
mobileSidebarOpen && [
"!left-docs_0.5 !top-docs_0.5 z-50 shadow-elevation-modal dark:shadow-elevation-modal-dark",
"rounded",
"lg:!left-0 lg:!top-0 lg:shadow-none",
],
desktopSidebarOpen && "lg:left-0",
!desktopSidebarOpen && "lg:!absolute lg:!-left-full",
className
)}
id="sidebar"
style={{
animationFillMode: "forwards",
}}
ref={sidebarWrapperRef}
>
{banner && <div className="mb-docs_1">{banner}</div>}
{sidebarItems.parentItem && (
<div className={clsx("mb-docs_1", !banner && "mt-docs_1.5")}>
<SidebarBack />
<SidebarTitle item={sidebarItems.parentItem} />
</div>
)}
<SwitchTransition>
<CSSTransition
key={sidebarItems.parentItem?.title || "home"}
nodeRef={sidebarRef}
classNames={{
enter: "animate-fadeInLeft animate-fast",
exit: "animate-fadeOutLeft animate-fast",
}}
timeout={200}
>
<div className="overflow-auto" ref={sidebarRef}>
<div className={clsx("mb-docs_1.5 lg:hidden")}>
{!sidebarItems.mobile.length && !staticSidebarItems && (
<Loading className="px-0" />
<ul className={clsx("h-full w-full", "flex flex-col")}>
<SidebarTop
{...sidebarTopProps}
parentItem={sidebarItems.parentItem}
ref={sidebarTopRef}
/>
<SwitchTransition>
<CSSTransition
key={sidebarItems.parentItem?.title || "home"}
nodeRef={sidebarRef}
classNames={{
enter: "animate-fadeInLeft animate-fast",
exit: "animate-fadeOutLeft animate-fast",
}}
timeout={200}
>
<div
className={clsx(
"overflow-y-scroll clip",
"py-docs_0.75 flex-1"
)}
{sidebarItems.mobile.map((item, index) => (
<SidebarItem
item={item}
key={index}
expandItems={expandItems}
sidebarHasParent={sidebarHasParent}
isMobile={true}
/>
))}
ref={sidebarRef}
style={{
maxHeight: `calc(100vh - ${sidebarTopHeight}px)`,
}}
id="sidebar"
>
{/* MOBILE SIDEBAR */}
<div className={clsx("lg:hidden")}>
{!sidebarItems.mobile.length && !staticSidebarItems && (
<Loading className="px-0" />
)}
{sidebarItems.mobile.map((item, index) => (
<SidebarItem
item={item}
key={index}
expandItems={expandItems}
hasNextItems={index !== sidebarItems.default.length - 1}
/>
))}
<SidebarSeparator />
</div>
{/* DESKTOP SIDEBAR */}
<div className="mt-docs_0.75 lg:mt-0">
{!sidebarItems.default.length && !staticSidebarItems && (
<Loading className="px-0" />
)}
{sidebarItems.default.map((item, index) => (
<SidebarItem
item={item}
key={index}
expandItems={expandItems}
hasNextItems={index !== sidebarItems.default.length - 1}
/>
))}
</div>
</div>
<div className={clsx("mb-docs_1.5")}>
{!sidebarItems.top.length && !staticSidebarItems && (
<Loading className="px-0" />
)}
{sidebarItems.top.map((item, index) => (
<SidebarItem
item={item}
key={index}
expandItems={expandItems}
sidebarHasParent={sidebarHasParent}
/>
))}
</div>
<div className="mb-docs_1.5">
{!sidebarItems.bottom.length && !staticSidebarItems && (
<Loading className="px-0" />
)}
{sidebarItems.bottom.map((item, index) => (
<SidebarItem
item={item}
key={index}
expandItems={expandItems}
sidebarHasParent={sidebarHasParent}
/>
))}
</div>
</div>
</CSSTransition>
</SwitchTransition>
</ul>
</aside>
</CSSTransition>
</SwitchTransition>
</ul>
</aside>
</>
)
}

View File

@@ -0,0 +1,33 @@
"use client"
import clsx from "clsx"
import React, { useMemo } from "react"
import { ToCItemUi } from "types"
import { TocList } from "../List"
export type TocItemProps = {
item: ToCItemUi
activeItem: string
}
export const TocItem = ({ item, activeItem }: TocItemProps) => {
const isActive = useMemo(() => item.id === activeItem, [item, activeItem])
return (
<li>
<span
className={clsx(
"h-docs_0.125 rounded-full transition-colors",
isActive && "bg-medusa-fg-subtle",
!isActive && "bg-medusa-fg-disabled",
item.level === 2 && "w-[20px]",
item.level === 3 && "w-[10px]",
"block"
)}
></span>
{(item.children?.length || 0) > 0 && (
<TocList items={item.children!} activeItem={activeItem} />
)}
</li>
)
}

View File

@@ -0,0 +1,29 @@
import clsx from "clsx"
import React from "react"
import { ToCItemUi } from "types"
import { TocItem } from "../Item"
export type TocListProps = {
items: ToCItemUi[]
topLevel?: boolean
activeItem: string
}
export const TocList = ({
items,
topLevel = false,
activeItem,
}: TocListProps) => {
return (
<ul
className={clsx(
"flex flex-col gap-docs_0.75 items-end",
!topLevel && "mt-docs_0.75"
)}
>
{items.map((item, key) => (
<TocItem item={item} key={key} activeItem={activeItem} />
))}
</ul>
)
}

View File

@@ -0,0 +1,80 @@
"use client"
import { EllipseMiniSolid } from "@medusajs/icons"
import clsx from "clsx"
import React from "react"
import { ToCItemUi } from "types"
import { Button, useScrollController } from "../../.."
export type TocMenuProps = {
items: ToCItemUi[]
activeItem: string
show: boolean
setShow: (value: boolean) => void
}
export const TocMenu = ({ items, activeItem, show, setShow }: TocMenuProps) => {
const { scrollToElement } = useScrollController()
const getItemElm = (item: ToCItemUi) => {
const isActive = item.id === activeItem
const hasChildren = item.children?.length || 0 > 0
return (
<li className={clsx("text-medusa-fg-base w-full")}>
<Button
variant="transparent-clear"
className={clsx(
"gap-docs_0.5 flex-1",
"cursor-pointer rounded-docs_sm py-docs_0.25",
"px-docs_0.5 hover:bg-medusa-bg-component-hover",
"!text-inherit max-w-full w-full",
"focus:!outline-none focus:!shadow-none focus:dark:!shadow-none",
"!flex !justify-start !items-center",
isActive && "!text-compact-small-plus",
!isActive && "!text-compact-small"
)}
onClick={() => {
history.pushState({}, "", `#${item.id}`)
const elm = document.getElementById(item.id) as HTMLElement
scrollToElement(elm)
}}
>
<EllipseMiniSolid className={clsx(!isActive && "invisible")} />
<span className="truncate flex-1 text-left">{item.title}</span>
</Button>
{hasChildren && (
<ul className="pl-docs_0.5">
{item.children!.map((childItem, index) => (
<React.Fragment key={index}>
{getItemElm(childItem)}
</React.Fragment>
))}
</ul>
)}
</li>
)
}
return (
<div
className={clsx(
"hidden lg:flex relative transition-[width] lg:h-full",
"w-0 z-50 bg-medusa-bg-subtle overflow-hidden flex flex-col justify-center",
show && "lg:w-toc"
)}
onMouseLeave={() => setShow(false)}
>
<ul
className={clsx(
"p-docs_0.75 lg:w-toc max-h-full overflow-y-scroll",
"absolute lg:-right-full transition-[right,opacity] opacity-0",
show && "lg:right-0 lg:opacity-100"
)}
>
{items.map((item, index) => (
<React.Fragment key={index}>{getItemElm(item)}</React.Fragment>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,99 @@
"use client"
import React, { useEffect, useState } from "react"
import { ToCItemUi } from "types"
import {
ActiveOnScrollItem,
isElmWindow,
useActiveOnScroll,
useIsBrowser,
useScrollController,
} from "../.."
import { TocList } from "./List"
import clsx from "clsx"
import { TocMenu } from "./Menu"
export const Toc = () => {
const [items, setItems] = useState<ToCItemUi[]>([])
const [showMenu, setShowMenu] = useState(false)
const isBrowser = useIsBrowser()
const { items: headingItems, activeItemId } = useActiveOnScroll({})
const [maxHeight, setMaxHeight] = useState(0)
const { scrollableElement } = useScrollController()
const formatHeadingContent = (content: string | null): string => {
return content?.replaceAll(/#$/g, "") || ""
}
const formatHeadingObject = ({
heading,
children,
}: ActiveOnScrollItem): ToCItemUi => {
const level = parseInt(heading.tagName.replace("H", ""))
return {
title: formatHeadingContent(heading.textContent),
id: heading.id,
level,
children: children?.map(formatHeadingObject),
associatedHeading: heading as HTMLHeadingElement,
}
}
useEffect(() => {
setItems(headingItems.map(formatHeadingObject))
}, [headingItems])
const handleResize = () => {
const offset =
(scrollableElement instanceof HTMLElement
? scrollableElement.offsetTop
: 0) + 56
setMaxHeight(
(isElmWindow(scrollableElement)
? scrollableElement.innerHeight
: scrollableElement?.clientHeight || 0) - offset
)
}
useEffect(() => {
if (!isBrowser) {
return
}
handleResize()
window.addEventListener("resize", handleResize)
return () => {
window.removeEventListener("resize", handleResize)
}
}, [isBrowser])
return (
<div className="hidden lg:block" onMouseOver={() => setShowMenu(true)}>
<div
className={clsx(
"fixed top-1/2 right-[20px]",
"hidden lg:flex justify-center items-center",
"overflow-hidden z-10",
showMenu && "lg:hidden",
maxHeight < 1000 && "-translate-y-[40%]",
maxHeight >= 1000 && "-translate-y-1/2"
)}
onMouseOver={() => setShowMenu(true)}
style={{
maxHeight,
}}
>
<TocList items={items} topLevel={true} activeItem={activeItemId} />
</div>
<TocMenu
items={items}
activeItem={activeItemId}
show={showMenu}
setShow={setShowMenu}
/>
</div>
)
}

View File

@@ -36,7 +36,8 @@ export const TypeList = ({
return (
<div
className={clsx(
"bg-docs-bg-surface shadow-elevation-card-rest rounded my-docs_1",
"bg-medusa-bg-subtle rounded my-docs_1",
"shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark",
className
)}
{...props}

View File

@@ -36,20 +36,14 @@ export * from "./Link"
export * from "./Loading"
export * from "./Loading/Dots"
export * from "./Loading/Spinner"
export * from "./MainNav"
export * from "./MarkdownContent"
export * from "./MDXComponents"
export * from "./Menu"
export * from "./MermaidDiagram"
export * from "./Modal"
export * from "./Modal/Header"
export * from "./Modal/Footer"
export * from "./Navbar"
export * from "./Navbar/ColorModeToggle"
export * from "./Navbar/IconButton"
export * from "./Navbar/Link"
export * from "./Navbar/Logo"
export * from "./Navbar/MobileMenu"
export * from "./Navbar/MobileMenu/Button"
export * from "./Navbar/SearchModalOpener"
export * from "./Note"
export * from "./Notification"
export * from "./Notification/Item"