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:
Shahed Nasser
2025-05-30 16:55:36 +03:00
committed by GitHub
parent 490bd7647f
commit 009d00f27d
293 changed files with 1975 additions and 506 deletions

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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"
)}
/>
)
}

View File

@@ -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>
)
}