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:
@@ -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>
|
||||
)
|
||||
}
|
||||
143
www/packages/docs-ui/src/components/ContentMenu/Toc/index.tsx
Normal file
143
www/packages/docs-ui/src/components/ContentMenu/Toc/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
32
www/packages/docs-ui/src/components/ContentMenu/index.tsx
Normal file
32
www/packages/docs-ui/src/components/ContentMenu/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user