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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user