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
+2
View File
@@ -1,3 +1,5 @@
export * from "./use-active-on-scroll"
export * from "./use-click-outside"
export * from "./use-collapsible"
export * from "./use-collapsible-code-lines"
export * from "./use-copy"
@@ -0,0 +1,159 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { isElmWindow, useIsBrowser, useScrollController } from "../.."
import { usePathname } from "next/navigation"
import { useMutationObserver } from "../use-mutation-observer"
export type ActiveOnScrollItem = {
heading: HTMLHeadingElement
children?: ActiveOnScrollItem[]
}
export type UseActiveOnScrollProps = {
rootElm?: Document | HTMLElement
enable?: boolean
useDefaultIfNoActive?: boolean
}
export const useActiveOnScroll = ({
rootElm,
enable = true,
useDefaultIfNoActive = true,
}: UseActiveOnScrollProps) => {
const [items, setItems] = useState<ActiveOnScrollItem[]>([])
const [activeItemId, setActiveItemId] = useState("")
const { scrollableElement } = useScrollController()
const isBrowser = useIsBrowser()
const pathname = usePathname()
const root = useMemo(() => {
if (!enable) {
return
}
if (rootElm) {
return rootElm
}
if (!isBrowser) {
return
}
return document
}, [rootElm, isBrowser, enable])
const getHeadingsInElm = useCallback(() => {
if (!isBrowser || !enable) {
return []
}
return root?.querySelectorAll("h2,h3")
}, [isBrowser, pathname, root, enable])
const setHeadingItems = useCallback(() => {
if (!enable) {
return
}
const headings = getHeadingsInElm()
const itemsToSet: ActiveOnScrollItem[] = []
let lastLevel2HeadingIndex = -1
headings?.forEach((heading) => {
const level = parseInt(heading.tagName.replace("H", ""))
const isLevel2 = level === 2
const headingItem: ActiveOnScrollItem = {
heading: heading as HTMLHeadingElement,
children: [],
}
if (isLevel2 || lastLevel2HeadingIndex === -1) {
itemsToSet.push(headingItem)
if (isLevel2) {
lastLevel2HeadingIndex = itemsToSet.length - 1
}
} else if (lastLevel2HeadingIndex !== -1) {
itemsToSet[lastLevel2HeadingIndex].children?.push(headingItem)
}
})
setItems(itemsToSet)
}, [getHeadingsInElm, enable])
useMutationObserver({
elm: root,
callback: setHeadingItems,
})
const setActiveToClosest = useCallback(() => {
if (!enable) {
return
}
const headings = getHeadingsInElm()
let closestPositiveHeading: HTMLHeadingElement | undefined = undefined
let closestNegativeHeading: HTMLHeadingElement | undefined = undefined
let closestPositiveDistance = Infinity
let closestNegativeDistance = -Infinity
const halfway = isElmWindow(scrollableElement)
? scrollableElement.innerHeight / 2
: scrollableElement
? scrollableElement.scrollHeight / 2
: 0
headings?.forEach((heading) => {
const headingDistance = heading.getBoundingClientRect().top
if (headingDistance > 0 && headingDistance < closestPositiveDistance) {
closestPositiveDistance = headingDistance
closestPositiveHeading = heading as HTMLHeadingElement
} else if (
headingDistance < 0 &&
headingDistance > closestNegativeDistance
) {
closestNegativeDistance = headingDistance
closestNegativeHeading = heading as HTMLHeadingElement
}
})
const negativeDistanceToHalfway = Math.abs(
halfway + closestNegativeDistance
)
const positiveDistanceToHalfway = Math.abs(
halfway - closestPositiveDistance
)
const chosenClosest =
negativeDistanceToHalfway > positiveDistanceToHalfway
? closestNegativeHeading
: closestPositiveHeading
setActiveItemId(
chosenClosest
? (chosenClosest as HTMLHeadingElement).id
: items.length
? useDefaultIfNoActive
? items[0].heading.id
: ""
: ""
)
}, [getHeadingsInElm, items, enable])
useEffect(() => {
if (!scrollableElement || !enable) {
return
}
scrollableElement.addEventListener("scroll", setActiveToClosest)
return () => {
scrollableElement.removeEventListener("scroll", setActiveToClosest)
}
}, [scrollableElement, setActiveToClosest, enable])
useEffect(() => {
if (items.length && enable) {
setActiveToClosest()
}
}, [items, setActiveToClosest, enable])
return {
items,
activeItemId,
}
}
@@ -0,0 +1,37 @@
"use client"
import React, { useCallback, useEffect } from "react"
import { useIsBrowser } from "../.."
export type UseClickOutsideProps = {
elmRef: React.RefObject<HTMLElement>
onClickOutside: (e: MouseEvent) => void
}
export const useClickOutside = ({
elmRef,
onClickOutside,
}: UseClickOutsideProps) => {
const isBrowser = useIsBrowser()
const checkClickOutside = useCallback(
(e: MouseEvent) => {
if (!elmRef.current?.contains(e.target as Node)) {
onClickOutside(e)
}
},
[elmRef.current, onClickOutside]
)
useEffect(() => {
if (!isBrowser) {
return
}
window.document.addEventListener("click", checkClickOutside)
return () => {
window.document.removeEventListener("click", checkClickOutside)
}
}, [isBrowser, checkClickOutside])
}
@@ -0,0 +1,31 @@
import { useEffect } from "react"
type UseMutationObserverProps = {
elm: Document | HTMLElement | undefined
callback: () => void
options?: {
attributes?: boolean
characterData?: boolean
childList?: boolean
subtree?: boolean
}
}
export const useMutationObserver = ({
elm,
callback,
options = {
attributes: true,
characterData: true,
childList: true,
subtree: true,
},
}: UseMutationObserverProps) => {
useEffect(() => {
if (elm) {
const observer = new MutationObserver(callback)
observer.observe(elm, options)
return () => observer.disconnect()
}
}, [callback, options])
}
@@ -59,6 +59,10 @@ type ScrollController = {
scrollableElement: Element | Window | undefined
/** Retrieves the scroll top if the scrollable element */
getScrolledTop: () => number
/** Scrolls to an element */
scrollToElement: (elm: HTMLElement) => void
/** Scrolls to a top value */
scrollToTop: (top: number, parentTop?: number) => void
}
function useScrollControllerContextValue({
@@ -83,6 +87,27 @@ function useScrollControllerContextValue({
return scrollableElement ? getScrolledTopUtil(scrollableElement) : 0
}
const scrollToElement = (elm: HTMLElement) => {
scrollToTop(elm.offsetTop)
}
const scrollToTop = (top: number, parentTop?: number) => {
const parentOffsetTop =
parentTop !== undefined
? parentTop
: isElmWindow(scrollableElement)
? 0
: scrollableElement instanceof HTMLElement
? scrollableElement.offsetTop
: 0
scrollableElement?.scrollTo({
// 56 is the height of the navbar
// might need a better way to determine it.
top: top - parentOffsetTop - 56,
})
}
return useMemo(
() => ({
scrollEventsEnabledRef,
@@ -94,6 +119,8 @@ function useScrollControllerContextValue({
},
scrollableElement,
getScrolledTop,
scrollToElement,
scrollToTop,
}),
[scrollableElement]
)