docs: redesign table of content (#12647)
* implement toc * added to projects * fixes and adapt for references * added product frontmatter * remove action menu from 404 pages
This commit is contained in:
@@ -142,10 +142,10 @@ export const AiAssistantChatWindow = () => {
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
"flex z-50 w-[calc(100%-8px)] md:w-ai-assistant transition-[height,right]",
|
||||
"flex z-50 w-[calc(100%-8px)] md:w-ai-assistant transition-[right]",
|
||||
"absolute -right-[150%] sm:-right-full top-0",
|
||||
type === "default" && [
|
||||
"xxl:w-0 xxl:relative xxl:transition-[height,right,width]",
|
||||
"xxl:w-0 xxl:relative xxl:transition-[right,width]",
|
||||
"xxl:shadow-elevation-card-rest xxl:dark:shadow-elevation-card-rest-dark",
|
||||
chatOpened && "xxl:!w-ai-assistant",
|
||||
],
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
import React from "react"
|
||||
import clsx from "clsx"
|
||||
import { CardProps } from "../.."
|
||||
import { BorderedIcon, ThemeImage, useIsExternalLink } from "../../../.."
|
||||
import {
|
||||
BorderedIcon,
|
||||
Button,
|
||||
ThemeImage,
|
||||
useIsExternalLink,
|
||||
} from "../../../.."
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { ArrowUpRightOnBox, TriangleRightMini } from "@medusajs/icons"
|
||||
import { ArrowUpRightOnBox, TriangleRightMini, XMark } from "@medusajs/icons"
|
||||
|
||||
export const CardLayoutMini = ({
|
||||
icon,
|
||||
@@ -15,6 +20,11 @@ export const CardLayoutMini = ({
|
||||
title,
|
||||
text,
|
||||
href,
|
||||
hrefProps = {},
|
||||
closeable = false,
|
||||
onClose,
|
||||
className,
|
||||
imageDimensions = { width: 45, height: 36 },
|
||||
}: CardProps) => {
|
||||
const isExternal = useIsExternalLink({ href })
|
||||
|
||||
@@ -26,7 +36,8 @@ export const CardLayoutMini = ({
|
||||
"hover:shadow-elevation-card-hover dark:hover:shadow-elevation-card-hover-dark",
|
||||
"bg-medusa-tag-neutral-bg dark:bg-medusa-bg-component",
|
||||
"hover:bg-medusa-tag-neutral-bg-hover dark:hover:bg-medusa-bg-component-hover",
|
||||
"w-fit transition-all"
|
||||
"w-fit transition-[shadow,background]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -48,12 +59,12 @@ export const CardLayoutMini = ({
|
||||
"shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark",
|
||||
"rounded-docs_xs"
|
||||
)}
|
||||
width={45}
|
||||
height={36}
|
||||
width={imageDimensions.width}
|
||||
height={imageDimensions.height}
|
||||
alt={title || text || ""}
|
||||
style={{
|
||||
width: "45px",
|
||||
height: "36px",
|
||||
width: `${imageDimensions.width}px`,
|
||||
height: `${imageDimensions.height}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -64,12 +75,12 @@ export const CardLayoutMini = ({
|
||||
"shadow-elevation-card-rest dark:shadow-elevation-card-rest-dark",
|
||||
"rounded-docs_xs"
|
||||
)}
|
||||
width={45}
|
||||
height={36}
|
||||
width={imageDimensions.width}
|
||||
height={imageDimensions.height}
|
||||
alt={title || text || ""}
|
||||
style={{
|
||||
width: "45px",
|
||||
height: "36px",
|
||||
width: `${imageDimensions.width}px`,
|
||||
height: `${imageDimensions.height}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -85,16 +96,28 @@ export const CardLayoutMini = ({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-medusa-fg-subtle">
|
||||
{isExternal ? <ArrowUpRightOnBox /> : <TriangleRightMini />}
|
||||
</span>
|
||||
{!closeable && (
|
||||
<span className="text-medusa-fg-subtle">
|
||||
{isExternal ? <ArrowUpRightOnBox /> : <TriangleRightMini />}
|
||||
</span>
|
||||
)}
|
||||
{href && (
|
||||
<Link
|
||||
href={href}
|
||||
className="absolute left-0 top-0 w-full h-full"
|
||||
className="absolute left-0 top-0 w-full h-full z-[1]"
|
||||
prefetch={false}
|
||||
{...hrefProps}
|
||||
/>
|
||||
)}
|
||||
{closeable && (
|
||||
<Button
|
||||
variant="transparent-clear"
|
||||
onClick={onClose}
|
||||
className="!p-[2.5px] z-[2] hover:!bg-transparent focus:!shadow-none focus:!bg-transparent"
|
||||
>
|
||||
<XMark />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IconProps } from "@medusajs/icons/dist/types"
|
||||
import { CardLargeLayout } from "./Layout/Large"
|
||||
import { CardFillerLayout } from "./Layout/Filler"
|
||||
import { CardLayoutMini } from "./Layout/Mini"
|
||||
import { LinkProps } from "next/link"
|
||||
|
||||
export type CardProps = {
|
||||
type?: "default" | "large" | "filler" | "mini"
|
||||
@@ -15,6 +16,10 @@ export type CardProps = {
|
||||
light: string
|
||||
dark: string
|
||||
}
|
||||
imageDimensions?: {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
title?: string
|
||||
text?: string
|
||||
href?: string
|
||||
@@ -24,6 +29,9 @@ export type CardProps = {
|
||||
children?: React.ReactNode
|
||||
badge?: BadgeProps
|
||||
highlightText?: string[]
|
||||
closeable?: boolean
|
||||
onClose?: () => void
|
||||
hrefProps?: Partial<LinkProps & React.AllHTMLAttributes<HTMLAnchorElement>>
|
||||
}
|
||||
|
||||
export const Card = ({ type = "default", ...props }: CardProps) => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import React from "react"
|
||||
import { MarkdownIcon } from "../../Icons/Markdown"
|
||||
import { useAiAssistant, useSiteConfig } from "../../../providers"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { BroomSparkle } from "@medusajs/icons"
|
||||
import { useAiAssistantChat } from "../../../providers/AiAssistant/Chat"
|
||||
|
||||
export const ContentMenuActions = () => {
|
||||
const {
|
||||
config: { baseUrl, basePath },
|
||||
} = useSiteConfig()
|
||||
const pathname = usePathname()
|
||||
const { setChatOpened } = useAiAssistant()
|
||||
const { setQuestion, loading } = useAiAssistantChat()
|
||||
const pageUrl = `${baseUrl}${basePath}${pathname}`
|
||||
|
||||
const handleAiAssistantClick = () => {
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
setQuestion(`Explain the page ${pageUrl}`)
|
||||
setChatOpened(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-docs_0.5">
|
||||
<Link
|
||||
className="flex items-center gap-docs_0.5 text-medusa-fg-subtle text-x-small-plus hover:text-medusa-fg-base"
|
||||
href={`${pageUrl}/index.html.md`}
|
||||
>
|
||||
<MarkdownIcon width={15} height={15} />
|
||||
View as Markdown
|
||||
</Link>
|
||||
<button
|
||||
className="appearance-none p-0 flex items-center gap-docs_0.5 text-medusa-fg-subtle text-x-small-plus hover:text-medusa-fg-base"
|
||||
onClick={handleAiAssistantClick}
|
||||
>
|
||||
<BroomSparkle width={15} height={15} />
|
||||
Explain with AI Assistant
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo } from "react"
|
||||
import { useSiteConfig } from "../../../providers"
|
||||
import { products } from "../../../constants"
|
||||
import { Product } from "types"
|
||||
import { BorderedIcon } from "../../BorderedIcon"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const ContentMenuProducts = () => {
|
||||
const { frontmatter, config } = useSiteConfig()
|
||||
|
||||
const loadedProducts = useMemo(() => {
|
||||
return frontmatter.products
|
||||
?.sort()
|
||||
.map((product) => {
|
||||
return products.find(
|
||||
(p) => p.name.toLowerCase() === product.toLowerCase()
|
||||
)
|
||||
})
|
||||
.filter(Boolean) as Product[]
|
||||
}, [frontmatter.products])
|
||||
|
||||
if (!loadedProducts?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const getProductUrl = (product: Product) => {
|
||||
return `${config.baseUrl}${product.path}`
|
||||
}
|
||||
|
||||
const getProductImageUrl = (product: Product) => {
|
||||
return `${config.basePath}${product.image}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-docs_0.5">
|
||||
<span className="text-x-small-plus text-medusa-fg-muted">
|
||||
Modules used
|
||||
</span>
|
||||
{loadedProducts.map((product, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={getProductUrl(product)}
|
||||
className="flex gap-docs_0.5 items-center"
|
||||
>
|
||||
<BorderedIcon
|
||||
wrapperClassName={clsx("bg-medusa-bg-base")}
|
||||
icon={getProductImageUrl(product)}
|
||||
/>
|
||||
<span className="text-medusa-fg-subtle text-x-small-plus">
|
||||
{product.title}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect } from "react"
|
||||
import { ToCItem, ToCItemUi } from "types"
|
||||
import {
|
||||
ActiveOnScrollItem,
|
||||
useActiveOnScroll,
|
||||
useScrollController,
|
||||
} from "../../../hooks"
|
||||
import clsx from "clsx"
|
||||
import Link from "next/link"
|
||||
import { useSiteConfig } from "../../../providers"
|
||||
import { Loading } from "../../Loading"
|
||||
|
||||
export const ContentMenuToc = () => {
|
||||
const { toc: items, frontmatter, setToc } = useSiteConfig()
|
||||
const { items: generatedItems, activeItemId } = useActiveOnScroll({
|
||||
maxLevel: 4,
|
||||
})
|
||||
|
||||
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(() => {
|
||||
if (
|
||||
frontmatter.generate_toc &&
|
||||
generatedItems &&
|
||||
items?.length !== generatedItems.length
|
||||
) {
|
||||
const tocItems: ToCItem[] = generatedItems.map(formatHeadingObject)
|
||||
setToc(tocItems)
|
||||
}
|
||||
}, [frontmatter, generatedItems])
|
||||
|
||||
useEffect(() => {
|
||||
const activeElement = document.querySelector(
|
||||
".toc-item a[href='#" + activeItemId + "']"
|
||||
) as HTMLAnchorElement
|
||||
if (!activeElement) {
|
||||
return
|
||||
}
|
||||
|
||||
activeElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "nearest",
|
||||
})
|
||||
}, [activeItemId])
|
||||
|
||||
if (items && !items.length) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-max max-h-full overflow-y-hidden flex relative flex-col">
|
||||
<div className="absolute -left-px top-0 h-full w-[1.5px] bg-medusa-border-base" />
|
||||
{items !== null && (
|
||||
<TocList
|
||||
items={items}
|
||||
activeItemId={activeItemId}
|
||||
className="relative overflow-y-auto"
|
||||
/>
|
||||
)}
|
||||
{items === null && <EmptyTocItems />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type TocListProps = {
|
||||
items: ToCItem[]
|
||||
activeItemId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TocList = ({ items, activeItemId, className }: TocListProps) => {
|
||||
return (
|
||||
<ul className={className}>
|
||||
{items.map((item) => (
|
||||
<TocItem item={item} key={item.id} activeItemId={activeItemId} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
type TocItemProps = {
|
||||
item: ToCItem
|
||||
activeItemId: string
|
||||
}
|
||||
|
||||
const TocItem = ({ item, activeItemId }: TocItemProps) => {
|
||||
const { scrollToElement } = useScrollController()
|
||||
return (
|
||||
<li className="w-full pt-docs_0.5 toc-item">
|
||||
<Link
|
||||
href={`#${item.id}`}
|
||||
className={clsx(
|
||||
"text-x-small-plus block w-full border-l-[1.5px]",
|
||||
item.id === activeItemId &&
|
||||
"border-medusa-fg-base text-medusa-fg-base",
|
||||
item.id !== activeItemId &&
|
||||
"text-medusa-fg-muted hover:text-medusa-fg-base border-transparent"
|
||||
)}
|
||||
style={{
|
||||
paddingLeft: `${(item.level - 1) * 12}px`,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
history.pushState({}, "", `#${item.id}`)
|
||||
const elm = document.getElementById(item.id) as HTMLElement
|
||||
scrollToElement(elm)
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
{(item.children?.length ?? 0) > 0 && (
|
||||
<TocList items={item.children!} activeItemId={activeItemId} />
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
const EmptyTocItems = () => {
|
||||
return (
|
||||
<div className="animate-pulse">
|
||||
<Loading count={5} className="pt-docs_0.5 px-docs_0.75 !my-0" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { Card } from "../../Card"
|
||||
import { useIsBrowser, useSiteConfig } from "../../../providers"
|
||||
import clsx from "clsx"
|
||||
|
||||
const LOCAL_STORAGE_KEY = "last-version"
|
||||
|
||||
export const ContentMenuVersion = () => {
|
||||
const {
|
||||
config: { version },
|
||||
} = useSiteConfig()
|
||||
const [showNewVersion, setShowNewVersion] = useState(false)
|
||||
const { isBrowser } = useIsBrowser()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser) {
|
||||
return
|
||||
}
|
||||
|
||||
const storedVersion = localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
if (storedVersion !== version.number) {
|
||||
setShowNewVersion(true)
|
||||
}
|
||||
}, [isBrowser])
|
||||
|
||||
const handleClose = () => {
|
||||
if (!showNewVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
setShowNewVersion(false)
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, version.number)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
type="mini"
|
||||
title={`New version`}
|
||||
text={`v${version.number} details`}
|
||||
closeable
|
||||
onClose={handleClose}
|
||||
href={version.releaseUrl}
|
||||
hrefProps={{
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
}}
|
||||
themeImage={version.bannerImage}
|
||||
imageDimensions={{
|
||||
width: 64,
|
||||
height: 40,
|
||||
}}
|
||||
className={clsx(
|
||||
"!border-0",
|
||||
(!showNewVersion || version.hide) && "invisible"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { ContentMenuVersion } from "./Version"
|
||||
import { ContentMenuToc } from "./Toc"
|
||||
import { ContentMenuActions } from "./Actions"
|
||||
import { ContentMenuProducts } from "./Products"
|
||||
import { useLayout } from "../../providers"
|
||||
|
||||
export const ContentMenu = () => {
|
||||
const { showCollapsedNavbar } = useLayout()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"hidden lg:flex w-full max-w-sidebar-lg",
|
||||
"flex-col gap-docs_2 pt-[28px] pb-docs_1.5 pr-docs_1",
|
||||
"fixed top-[57px] right-docs_0.25 z-10",
|
||||
showCollapsedNavbar && "h-[calc(100%-112px)]",
|
||||
!showCollapsedNavbar && "h-[calc(100%-56px)]"
|
||||
)}
|
||||
>
|
||||
<ContentMenuVersion />
|
||||
<div className="flex flex-col gap-docs_1.5 flex-1 overflow-auto">
|
||||
<ContentMenuToc />
|
||||
<ContentMenuActions />
|
||||
<ContentMenuProducts />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { LlmDropdown } from "../../LlmDropdown"
|
||||
|
||||
export type H1Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
id?: string
|
||||
hideLlmDropdown?: boolean
|
||||
}
|
||||
|
||||
export const H1 = ({ className, hideLlmDropdown, ...props }: H1Props) => {
|
||||
export const H1 = ({ className, ...props }: H1Props) => {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-2 h1-wrapper">
|
||||
<h1
|
||||
@@ -18,7 +16,6 @@ export const H1 = ({ className, hideLlmDropdown, ...props }: H1Props) => {
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{!hideLlmDropdown && <LlmDropdown />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect } from "react"
|
||||
import { FrontMatter, ToCItem } from "types"
|
||||
import { useSiteConfig } from "../../providers"
|
||||
|
||||
type InjectedMDXDataProps = {
|
||||
frontmatter: FrontMatter
|
||||
toc: ToCItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is injected by a recma plugin into MDX documents.
|
||||
*/
|
||||
export const InjectedMDXData = ({ frontmatter, toc }: InjectedMDXDataProps) => {
|
||||
const { setFrontmatter, setToc } = useSiteConfig()
|
||||
|
||||
useEffect(() => {
|
||||
setFrontmatter(frontmatter)
|
||||
}, [frontmatter])
|
||||
|
||||
useEffect(() => {
|
||||
setToc(toc)
|
||||
}, [toc])
|
||||
|
||||
return <></>
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export type LinkProps = Partial<NextLinkProps> &
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
withIcon?: boolean
|
||||
variant?: "default" | "content"
|
||||
}
|
||||
|
||||
export const Link = ({
|
||||
@@ -17,6 +18,7 @@ export const Link = ({
|
||||
children,
|
||||
className,
|
||||
withIcon = false,
|
||||
variant = "default",
|
||||
...rest
|
||||
}: LinkProps) => {
|
||||
if (href?.replace(/#.*$/, "").endsWith("page.mdx")) {
|
||||
@@ -27,7 +29,12 @@ export const Link = ({
|
||||
href={href || ""}
|
||||
{...rest}
|
||||
className={clsx(
|
||||
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
|
||||
variant === "default" &&
|
||||
"text-medusa-fg-interactive hover:text-medusa-fg-interactive-hover",
|
||||
variant === "content" && [
|
||||
"border-b border-medusa-border-strong hover:border-medusa-fg-interactive",
|
||||
"transition-all duration-200",
|
||||
],
|
||||
withIcon && "flex gap-0.25 items-center group",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React, { useRef, useState } from "react"
|
||||
import { useAiAssistant, useSiteConfig } from "../../providers"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { AiAssistent, Book } from "@medusajs/icons"
|
||||
import { DropdownMenu, Menu } from "../Menu"
|
||||
import { MarkdownIcon } from "../Icons/Markdown"
|
||||
import { useAiAssistantChat } from "../../providers/AiAssistant/Chat"
|
||||
import clsx from "clsx"
|
||||
import { useClickOutside } from "../.."
|
||||
|
||||
export const LlmDropdown = () => {
|
||||
const {
|
||||
config: { baseUrl, basePath },
|
||||
} = useSiteConfig()
|
||||
const pathname = usePathname()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { setChatOpened } = useAiAssistant()
|
||||
const { setQuestion, loading } = useAiAssistantChat()
|
||||
const ref = useRef<HTMLButtonElement | null>(null)
|
||||
useClickOutside({
|
||||
elmRef: ref,
|
||||
onClickOutside: () => {
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const pageUrl = `${baseUrl}${basePath}${pathname}`
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
dropdownButtonContent={<Book />}
|
||||
menuComponent={
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
type: "link",
|
||||
title: "View as Markdown",
|
||||
link: `${pageUrl}/index.html.md`,
|
||||
icon: <MarkdownIcon width={19} height={19} />,
|
||||
openInNewTab: true,
|
||||
},
|
||||
{
|
||||
type: "action",
|
||||
title: "Ask AI Assistant",
|
||||
action: () => {
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
setQuestion(`Explain the page ${pageUrl}`)
|
||||
setChatOpened(true)
|
||||
setOpen(false)
|
||||
},
|
||||
icon: <AiAssistent />,
|
||||
},
|
||||
]}
|
||||
className={clsx(
|
||||
"absolute right-0 top-[calc(100%+8px)] w-max",
|
||||
!open && "hidden"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
className="hidden md:block"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export const MainNavItemDropdown = ({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"cursor-pointer flex gap-docs_0.25 items-center",
|
||||
"cursor-pointer flex gap-docs_0.25 items-center px-docs_0.25 py-docs_0.5",
|
||||
isActive && "text-medusa-fg-base",
|
||||
!isActive && [
|
||||
"text-medusa-fg-muted hover:text-medusa-fg-subtle",
|
||||
|
||||
@@ -1,63 +1,27 @@
|
||||
"use state"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useIsBrowser, useSiteConfig } from "../../../providers"
|
||||
import React from "react"
|
||||
import { useSiteConfig } from "../../../providers"
|
||||
import Link from "next/link"
|
||||
import { Tooltip } from "../../Tooltip"
|
||||
import clsx from "clsx"
|
||||
|
||||
const LOCAL_STORAGE_SUFFIX = "last-version"
|
||||
|
||||
export const MainNavVersion = () => {
|
||||
const {
|
||||
config: { version },
|
||||
} = useSiteConfig()
|
||||
const [showNewBadge, setShowNewBadge] = useState(false)
|
||||
const { isBrowser } = useIsBrowser()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser) {
|
||||
return
|
||||
}
|
||||
|
||||
const storedVersion = localStorage.getItem(LOCAL_STORAGE_SUFFIX)
|
||||
if (storedVersion !== version.number) {
|
||||
setShowNewBadge(true)
|
||||
}
|
||||
}, [isBrowser])
|
||||
|
||||
const afterHover = () => {
|
||||
if (!showNewBadge) {
|
||||
return
|
||||
}
|
||||
|
||||
setShowNewBadge(false)
|
||||
localStorage.setItem(LOCAL_STORAGE_SUFFIX, version.number)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={version.releaseUrl}
|
||||
target="_blank"
|
||||
className={clsx(version.hide && "hidden")}
|
||||
className={clsx(version.hide && "hidden", "px-docs_0.5 py-docs_0.25")}
|
||||
>
|
||||
<Tooltip html="View the release notes<br/>on GitHub">
|
||||
<span
|
||||
className="relative text-compact-small-plus block"
|
||||
onMouseOut={afterHover}
|
||||
>
|
||||
<span className="relative text-compact-small-plus block">
|
||||
<span className="flex justify-center items-center">
|
||||
v{version.number}
|
||||
</span>
|
||||
{showNewBadge && (
|
||||
<span
|
||||
className={clsx(
|
||||
"bg-medusa-tag-blue-icon w-[10px] h-[10px]",
|
||||
"absolute -top-docs_0.25 -right-docs_0.5",
|
||||
"animate-pulse rounded-full"
|
||||
)}
|
||||
></span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
|
||||
@@ -71,14 +71,6 @@ export const MainNav = ({ className, itemsClassName }: MainNavProps) => {
|
||||
>
|
||||
<div className="lg:flex items-center gap-[6px] text-medusa-fg-subtle hidden">
|
||||
<MainNavVersion />
|
||||
<span
|
||||
className={clsx(
|
||||
"text-compact-small",
|
||||
config.version.hide && "hidden"
|
||||
)}
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<MainNavItemDropdown
|
||||
item={{
|
||||
type: "dropdown",
|
||||
@@ -113,9 +105,6 @@ export const MainNav = ({ className, itemsClassName }: MainNavProps) => {
|
||||
className="text-medusa-fg-subtle"
|
||||
wrapperClassName="z-10"
|
||||
/>
|
||||
{!showCollapsedNavbar && (
|
||||
<span className={clsx("text-compact-small")}>·</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<AiAssistantTriggerButton />
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, { useEffect, useMemo, useState } from "react"
|
||||
import { Sidebar } from "types"
|
||||
import { Badge, Loading, SidebarItem, useSidebar } from "../../../.."
|
||||
import clsx from "clsx"
|
||||
import { MinusMini, PlusMini } from "@medusajs/icons"
|
||||
import { TriangleDownMini, TriangleUpMini } from "@medusajs/icons"
|
||||
|
||||
export type SidebarItemCategoryProps = {
|
||||
item: Sidebar.SidebarItemCategory
|
||||
@@ -109,8 +109,8 @@ export const SidebarItemCategory = ({
|
||||
)}
|
||||
{!item.additionalElms && (
|
||||
<>
|
||||
{open && <MinusMini />}
|
||||
{!open && <PlusMini />}
|
||||
{open && <TriangleDownMini />}
|
||||
{!open && <TriangleUpMini />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { ToCItemUi } from "types"
|
||||
import {
|
||||
ActiveOnScrollItem,
|
||||
isElmWindow,
|
||||
useActiveOnScroll,
|
||||
useIsBrowser,
|
||||
useLayout,
|
||||
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 { showCollapsedNavbar } = useLayout()
|
||||
|
||||
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 extraMargin = showCollapsedNavbar ? 112 : 56
|
||||
const offset =
|
||||
(scrollableElement instanceof HTMLElement
|
||||
? scrollableElement.offsetTop
|
||||
: 0) + extraMargin
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export * from "./Heading"
|
||||
export * from "./HooksLoader"
|
||||
export * from "./IconHeadline"
|
||||
export * from "./Icons"
|
||||
export * from "./InjectedMDXData"
|
||||
export * from "./InlineIcon"
|
||||
export * from "./InlineThemeImage"
|
||||
export * from "./InlineCode"
|
||||
|
||||
Reference in New Issue
Block a user