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
@@ -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"
)}
>
&#183;
</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")}>&#183;</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"