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:
Shahed Nasser
2024-08-15 12:13:13 +03:00
committed by GitHub
parent 4cb28531e5
commit b4f3b8a79d
157 changed files with 5080 additions and 2010 deletions

View File

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

View File

@@ -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} />,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -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 (
<>