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,73 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import { InView } from "react-intersection-observer"
|
||||
import { isElmWindow, useScrollController, useSidebar } from "docs-ui"
|
||||
import checkElementInViewport from "../../../utils/check-element-in-viewport"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useScrollController, useSidebar } from "docs-ui"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import getSectionId from "../../../utils/get-section-id"
|
||||
|
||||
type H2Props = {
|
||||
addToSidebar?: boolean
|
||||
} & React.HTMLAttributes<HTMLHeadingElement>
|
||||
type H2Props = React.HTMLAttributes<HTMLHeadingElement>
|
||||
|
||||
const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
|
||||
const { activePath, setActivePath, addItems } = useSidebar()
|
||||
const { getScrolledTop, scrollableElement } = useScrollController()
|
||||
const H2 = ({ children, ...props }: H2Props) => {
|
||||
const headingRef = useRef<HTMLHeadingElement>(null)
|
||||
const { activePath, addItems } = useSidebar()
|
||||
const { scrollableElement, scrollToElement } = useScrollController()
|
||||
const [scrolledFirstTime, setScrolledFirstTime] = useState(false)
|
||||
|
||||
const handleViewChange = (
|
||||
inView: boolean,
|
||||
entry: IntersectionObserverEntry
|
||||
) => {
|
||||
if (!addToSidebar) {
|
||||
return
|
||||
}
|
||||
const heading = entry.target
|
||||
if (
|
||||
(inView ||
|
||||
checkElementInViewport(heading.parentElement || heading, 40)) &&
|
||||
getScrolledTop() !== 0 &&
|
||||
activePath !== heading.id
|
||||
) {
|
||||
// can't use next router as it doesn't support
|
||||
// changing url without scrolling
|
||||
history.pushState({}, "", `#${heading.id}`)
|
||||
setActivePath(heading.id)
|
||||
}
|
||||
}
|
||||
const id = getSectionId([children as string])
|
||||
const id = useMemo(() => getSectionId([children as string]), [children])
|
||||
|
||||
useEffect(() => {
|
||||
if (id === (activePath || location.hash.replace("#", ""))) {
|
||||
const elm = document.getElementById(id)
|
||||
elm?.scrollIntoView()
|
||||
if (!scrollableElement || !headingRef.current || scrolledFirstTime) {
|
||||
return
|
||||
}
|
||||
|
||||
if (id === (activePath || location.hash.replace("#", ""))) {
|
||||
scrollToElement(
|
||||
(headingRef.current.offsetParent as HTMLElement) || headingRef.current
|
||||
)
|
||||
}
|
||||
setScrolledFirstTime(scrolledFirstTime)
|
||||
}, [scrollableElement, headingRef, id])
|
||||
|
||||
useEffect(() => {
|
||||
addItems([
|
||||
{
|
||||
type: "link",
|
||||
path: `${id}`,
|
||||
title: children as string,
|
||||
loaded: true,
|
||||
},
|
||||
])
|
||||
}, [])
|
||||
|
||||
const root = useMemo(() => {
|
||||
return isElmWindow(scrollableElement) ? document.body : scrollableElement
|
||||
}, [scrollableElement])
|
||||
}, [id])
|
||||
|
||||
return (
|
||||
<InView
|
||||
as="h2"
|
||||
threshold={0.4}
|
||||
skip={!addToSidebar}
|
||||
initialInView={false}
|
||||
{...props}
|
||||
onChange={handleViewChange}
|
||||
id={id}
|
||||
root={root}
|
||||
>
|
||||
<h2 {...props} id={id} ref={headingRef}>
|
||||
{children}
|
||||
</InView>
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ const getCustomComponents = (scope?: ScopeType): MDXComponents => {
|
||||
...UiMDXComponents,
|
||||
Security: () => <Security specs={scope?.specs} />,
|
||||
a: Link,
|
||||
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<H2 addToSidebar={scope?.addToSidebar} {...props} />
|
||||
),
|
||||
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => <H2 {...props} />,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Button, useModal, usePageLoading } from "docs-ui"
|
||||
import DetailedFeedback from "../../DetailedFeedback"
|
||||
|
||||
const FeedbackModal = () => {
|
||||
const { setModalProps } = useModal()
|
||||
const { isLoading } = usePageLoading()
|
||||
|
||||
const openModal = () => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
setModalProps({
|
||||
title: "Send your Feedback",
|
||||
children: <DetailedFeedback />,
|
||||
contentClassName: "lg:!min-h-auto !p-0",
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={openModal} variant="secondary">
|
||||
Feedback
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default FeedbackModal
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Navbar as UiNavbar,
|
||||
getNavbarItems,
|
||||
usePageLoading,
|
||||
useSidebar,
|
||||
} from "docs-ui"
|
||||
import FeedbackModal from "./FeedbackModal"
|
||||
import { useMemo } from "react"
|
||||
import { config } from "../../config"
|
||||
import { usePathname } from "next/navigation"
|
||||
import VersionSwitcher from "../VersionSwitcher"
|
||||
import basePathUrl from "../../utils/base-path-url"
|
||||
|
||||
const Navbar = () => {
|
||||
const { setMobileSidebarOpen, mobileSidebarOpen } = useSidebar()
|
||||
const pathname = usePathname()
|
||||
const { isLoading } = usePageLoading()
|
||||
|
||||
const navbarItems = useMemo(
|
||||
() =>
|
||||
getNavbarItems({
|
||||
basePath: config.baseUrl,
|
||||
activePath: basePathUrl(pathname),
|
||||
version: "v2",
|
||||
}),
|
||||
[pathname]
|
||||
)
|
||||
|
||||
return (
|
||||
<UiNavbar
|
||||
logo={{
|
||||
light: basePathUrl("/images/logo-icon.png"),
|
||||
dark: basePathUrl("/images/logo-icon-dark.png"),
|
||||
}}
|
||||
items={navbarItems}
|
||||
mobileMenuButton={{
|
||||
setMobileSidebarOpen,
|
||||
mobileSidebarOpen,
|
||||
}}
|
||||
additionalActionsBefore={<VersionSwitcher />}
|
||||
additionalActionsAfter={<FeedbackModal />}
|
||||
showSearchOpener
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
@@ -1,14 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import clsx from "clsx"
|
||||
import { useActiveOnScroll, useSidebar } from "docs-ui"
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
export type SectionProps = {
|
||||
addToSidebar?: boolean
|
||||
checkActiveOnScroll?: boolean
|
||||
} & React.AllHTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Section = ({ children, className }: SectionProps) => {
|
||||
const Section = ({
|
||||
children,
|
||||
className,
|
||||
checkActiveOnScroll = false,
|
||||
}: SectionProps) => {
|
||||
const sectionRef = useRef<HTMLDivElement>(null)
|
||||
const { activeItemId } = useActiveOnScroll({
|
||||
rootElm: sectionRef.current || undefined,
|
||||
enable: checkActiveOnScroll,
|
||||
useDefaultIfNoActive: false,
|
||||
})
|
||||
const { setActivePath } = useSidebar()
|
||||
|
||||
useEffect(() => {
|
||||
if ("scrollRestoration" in history) {
|
||||
@@ -17,6 +28,13 @@ const Section = ({ children, className }: SectionProps) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeItemId.length) {
|
||||
history.pushState({}, "", `#${activeItemId}`)
|
||||
setActivePath(activeItemId)
|
||||
}
|
||||
}, [activeItemId])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={sectionRef}
|
||||
|
||||
@@ -13,6 +13,7 @@ import TagsOperationDescriptionSection from "./DescriptionSection"
|
||||
import DividedLayout from "@/layouts/Divided"
|
||||
import { useLoading } from "@/providers/loading"
|
||||
import SectionDivider from "../../Section/Divider"
|
||||
import checkElementInViewport from "../../../utils/check-element-in-viewport"
|
||||
|
||||
const TagOperationCodeSection = dynamic<TagOperationCodeSectionProps>(
|
||||
async () => import("./CodeSection")
|
||||
@@ -40,7 +41,7 @@ const TagOperation = ({
|
||||
)
|
||||
const nodeRef = useRef<Element | null>(null)
|
||||
const { loading, removeLoading } = useLoading()
|
||||
const { scrollableElement } = useScrollController()
|
||||
const { scrollableElement, scrollToTop } = useScrollController()
|
||||
const root = useMemo(() => {
|
||||
return isElmWindow(scrollableElement) ? document.body : scrollableElement
|
||||
}, [scrollableElement])
|
||||
@@ -75,24 +76,28 @@ const TagOperation = ({
|
||||
[ref]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const enableShow = () => {
|
||||
setShow(true)
|
||||
const scrollIntoView = useCallback(() => {
|
||||
if (nodeRef.current && !checkElementInViewport(nodeRef.current, 10)) {
|
||||
const elm = nodeRef.current as HTMLElement
|
||||
scrollToTop(
|
||||
elm.offsetTop + (elm.offsetParent as HTMLElement)?.offsetTop,
|
||||
0
|
||||
)
|
||||
}
|
||||
setShow(true)
|
||||
}, [scrollToTop, nodeRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeRef && nodeRef.current) {
|
||||
removeLoading()
|
||||
const currentHash = location.hash.replace("#", "")
|
||||
if (currentHash === path) {
|
||||
setTimeout(() => {
|
||||
nodeRef.current?.scrollIntoView()
|
||||
enableShow()
|
||||
}, 100)
|
||||
setTimeout(scrollIntoView, 100)
|
||||
} else if (currentHash.split("_")[0] === path.split("_")[0]) {
|
||||
enableShow()
|
||||
setShow(true)
|
||||
}
|
||||
}
|
||||
}, [nodeRef, path])
|
||||
}, [nodeRef, path, scrollIntoView])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useBaseSpecs } from "@/providers/base-specs"
|
||||
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
|
||||
import { useLoading } from "@/providers/loading"
|
||||
import DividedLoading from "@/components/DividedLoading"
|
||||
import { SidebarItemSections, SidebarItemType } from "types"
|
||||
import { SidebarItemSections, SidebarItem, SidebarItemCategory } from "types"
|
||||
import basePathUrl from "../../../utils/base-path-url"
|
||||
|
||||
const TagOperation = dynamic<TagOperationProps>(
|
||||
@@ -56,16 +56,17 @@ const TagPaths = ({ tag, className }: TagPathsProps) => {
|
||||
useEffect(() => {
|
||||
if (paths) {
|
||||
const parentItem = findItemInSection(
|
||||
items[SidebarItemSections.BOTTOM],
|
||||
{ path: tagSlugName },
|
||||
items[SidebarItemSections.DEFAULT],
|
||||
{ title: tag.name },
|
||||
false
|
||||
)
|
||||
const pathItems: SidebarItemType[] = getTagChildSidebarItems(paths)
|
||||
) as SidebarItemCategory
|
||||
const pathItems: SidebarItem[] = getTagChildSidebarItems(paths)
|
||||
if ((parentItem?.children?.length || 0) < pathItems.length) {
|
||||
addItems(pathItems, {
|
||||
section: SidebarItemSections.BOTTOM,
|
||||
section: SidebarItemSections.DEFAULT,
|
||||
parent: {
|
||||
path: tagSlugName,
|
||||
title: tag.name,
|
||||
path: "",
|
||||
changeLoaded: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
|
||||
},
|
||||
})
|
||||
|
||||
const { scrollableElement } = useScrollController()
|
||||
const { scrollableElement, scrollToElement } = useScrollController()
|
||||
const root = useMemo(() => {
|
||||
return isElmWindow(scrollableElement) ? document.body : scrollableElement
|
||||
}, [scrollableElement])
|
||||
@@ -49,6 +49,7 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
|
||||
addItems(
|
||||
[
|
||||
{
|
||||
type: "link",
|
||||
path: schemaSlug,
|
||||
title: `${formattedName} Object`,
|
||||
additionalElms: <Badge variant="neutral">Schema</Badge>,
|
||||
@@ -56,8 +57,9 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
|
||||
},
|
||||
],
|
||||
{
|
||||
section: SidebarItemSections.BOTTOM,
|
||||
section: SidebarItemSections.DEFAULT,
|
||||
parent: {
|
||||
title: tagName,
|
||||
path: tagSlugName,
|
||||
changeLoaded: true,
|
||||
},
|
||||
@@ -69,12 +71,12 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (schemaSlug === (activePath || location.hash.replace("#", ""))) {
|
||||
const elm = document.getElementById(schemaSlug)
|
||||
elm?.scrollIntoView({
|
||||
block: "center",
|
||||
})
|
||||
const elm = document.getElementById(schemaSlug) as HTMLElement
|
||||
if (!checkElementInViewport(elm, 40)) {
|
||||
scrollToElement(elm)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [activePath, schemaSlug])
|
||||
|
||||
const handleViewChange = (
|
||||
inView: boolean,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SchemaObject, TagObject } from "@/types/openapi"
|
||||
import useSWR from "swr"
|
||||
import { TagSectionSchemaProps } from "./Schema"
|
||||
import basePathUrl from "../../../utils/base-path-url"
|
||||
import checkElementInViewport from "../../../utils/check-element-in-viewport"
|
||||
|
||||
export type TagSectionProps = {
|
||||
tag: TagObject
|
||||
@@ -52,7 +53,7 @@ const TagSection = ({ tag }: TagSectionProps) => {
|
||||
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
|
||||
const { area } = useArea()
|
||||
const pathname = usePathname()
|
||||
const { scrollableElement } = useScrollController()
|
||||
const { scrollableElement, scrollToElement } = useScrollController()
|
||||
const { data } = useSWR<{
|
||||
schema: SchemaObject
|
||||
}>(
|
||||
@@ -96,8 +97,10 @@ const TagSection = ({ tag }: TagSectionProps) => {
|
||||
if (activePath && activePath.includes(slugTagName)) {
|
||||
const tagName = activePath.split("_")
|
||||
if (tagName.length === 1 && tagName[0] === slugTagName) {
|
||||
const elm = document.getElementById(tagName[0]) as Element
|
||||
elm?.scrollIntoView()
|
||||
const elm = document.getElementById(tagName[0])
|
||||
if (elm && !checkElementInViewport(elm, 10)) {
|
||||
scrollToElement(elm)
|
||||
}
|
||||
} else if (tagName.length > 1 && tagName[0] === slugTagName) {
|
||||
setLoadPaths(true)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { swrFetcher, useSidebar } from "docs-ui"
|
||||
import getSectionId from "@/utils/get-section-id"
|
||||
import { ExpandedDocument } from "@/types/openapi"
|
||||
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
|
||||
import { SidebarItemSections } from "types"
|
||||
import { SidebarItem, SidebarItemSections } from "types"
|
||||
import basePathUrl from "../../utils/base-path-url"
|
||||
|
||||
const TagSection = dynamic<TagSectionProps>(
|
||||
@@ -31,7 +31,7 @@ const Tags = () => {
|
||||
const [loadData, setLoadData] = useState<boolean>(false)
|
||||
const [expand, setExpand] = useState<string>("")
|
||||
const { baseSpecs, setBaseSpecs } = useBaseSpecs()
|
||||
const { addItems } = useSidebar()
|
||||
const { addItems, setActivePath } = useSidebar()
|
||||
const { area, prevArea } = useArea()
|
||||
|
||||
const { data } = useSWR<ExpandedDocument>(
|
||||
@@ -65,30 +65,42 @@ const Tags = () => {
|
||||
if (baseSpecs) {
|
||||
if (prevArea !== area) {
|
||||
setBaseSpecs(null)
|
||||
setLoadData(true)
|
||||
return
|
||||
}
|
||||
|
||||
addItems(
|
||||
baseSpecs.tags?.map((tag) => {
|
||||
const itemsToAdd: SidebarItem[] = [
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
]
|
||||
|
||||
if (baseSpecs.tags) {
|
||||
baseSpecs.tags.forEach((tag) => {
|
||||
const tagPathName = getSectionId([tag.name.toLowerCase()])
|
||||
const childItems =
|
||||
baseSpecs.expandedTags &&
|
||||
Object.hasOwn(baseSpecs.expandedTags, tagPathName)
|
||||
? getTagChildSidebarItems(baseSpecs.expandedTags[tagPathName])
|
||||
: []
|
||||
return {
|
||||
path: tagPathName,
|
||||
itemsToAdd.push({
|
||||
type: "category",
|
||||
title: tag.name,
|
||||
children: childItems,
|
||||
loaded: childItems.length > 0,
|
||||
}
|
||||
}) || [],
|
||||
{
|
||||
section: SidebarItemSections.BOTTOM,
|
||||
}
|
||||
)
|
||||
onOpen: () => {
|
||||
history.pushState({}, "", `#${tagPathName}`)
|
||||
setActivePath(tagPathName)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
addItems(itemsToAdd, {
|
||||
section: SidebarItemSections.DEFAULT,
|
||||
})
|
||||
}
|
||||
}, [baseSpecs, addItems])
|
||||
}, [baseSpecs, prevArea, area])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user