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
@@ -0,0 +1,51 @@
"use client"
import React, { createContext, useContext, useMemo, useState } from "react"
import { NavigationDropdownItem } from "types"
export type MainNavContext = {
navItems: NavigationDropdownItem[]
activeItem?: NavigationDropdownItem
reportIssueLink: string
}
const MainNavContext = createContext<MainNavContext | null>(null)
export type MainNavProviderProps = {
navItems: NavigationDropdownItem[]
reportIssueLink: string
children?: React.ReactNode
}
export const MainNavProvider = ({
navItems,
reportIssueLink,
children,
}: MainNavProviderProps) => {
const activeItem = useMemo(
() => navItems.find((item) => item.type === "link" && item.isActive),
[navItems]
)
return (
<MainNavContext.Provider
value={{
navItems,
activeItem,
reportIssueLink,
}}
>
{children}
</MainNavContext.Provider>
)
}
export const useMainNav = () => {
const context = useContext(MainNavContext)
if (!context) {
throw new Error("useMainNav must be used within a MainNavProvider")
}
return context
}
@@ -22,9 +22,9 @@ export const MobileProvider = ({ children }: MobileProviderProps) => {
const [isMobile, setIsMobile] = useState(false)
const handleResize = useCallback(() => {
if (window.innerWidth < 1025 && !isMobile) {
if (window.innerWidth < 1024 && !isMobile) {
setIsMobile(true)
} else if (window.innerWidth >= 1025 && isMobile) {
} else if (window.innerWidth >= 1024 && isMobile) {
setIsMobile(false)
}
}, [isMobile])
@@ -1,55 +0,0 @@
"use client"
import React, { createContext, useContext, useState, useEffect } from "react"
import { usePathname } from "next/navigation"
export type NavbarContextType = {
activeItem: string | null
setActiveItem: (value: string) => void
}
const NavbarContext = createContext<NavbarContextType | null>(null)
export type NavbarProviderProps = {
children: React.ReactNode
basePath?: string
}
export const NavbarProvider = ({
children,
basePath = "",
}: NavbarProviderProps) => {
const [activeItem, setActiveItem] = useState<string | null>(null)
const pathname = usePathname()
const assemblePathName = (path: string) =>
`${basePath}/${path.charAt(0) === "/" ? path.substring(1) : path}`
useEffect(() => {
const newPath = assemblePathName(pathname)
if (activeItem !== newPath) {
setActiveItem(newPath)
}
}, [pathname, activeItem])
return (
<NavbarContext.Provider
value={{
activeItem,
setActiveItem,
}}
>
{children}
</NavbarContext.Provider>
)
}
export const useNavbar = (): NavbarContextType => {
const context = useContext(NavbarContext)
if (!context) {
throw new Error("useNavbar must be used inside a NavbarProvider")
}
return context
}
@@ -9,7 +9,7 @@ import React, {
} from "react"
import { useSidebar } from "../Sidebar"
import { usePrevious } from "@uidotdev/usehooks"
import { SidebarItemType } from "types"
import { InteractiveSidebarItem, SidebarItem } from "types"
export type Page = {
title: string
@@ -27,8 +27,8 @@ export const PaginationContext = createContext<PaginationContextType | null>(
null
)
type SidebarItemWithParent = SidebarItemType & {
parent?: SidebarItemType
type SidebarItemWithParent = InteractiveSidebarItem & {
parent?: SidebarItem
}
type SearchItemsResult = {
@@ -43,20 +43,20 @@ export type PaginationProviderProps = {
export const PaginationProvider = ({ children }: PaginationProviderProps) => {
const { items, activePath } = useSidebar()
const combinedItems = useMemo(() => [...items.top, ...items.bottom], [items])
const combinedItems = useMemo(() => [...items.default], [items])
const previousActivePath = usePrevious(activePath)
const [nextPage, setNextPage] = useState<Page | undefined>()
const [prevPage, setPrevPage] = useState<Page | undefined>()
const getFirstChild = (
item: SidebarItemType
item: InteractiveSidebarItem
): SidebarItemWithParent | undefined => {
const children = getChildrenWithPages(item)
if (!children?.length) {
return undefined
}
return children[0].path
return children[0].type === "link"
? {
...children[0],
parent: item,
@@ -65,16 +65,18 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
}
const getChildrenWithPages = (
item: SidebarItemType
item: InteractiveSidebarItem
): SidebarItemWithParent[] | undefined => {
return item.children?.filter(
(childItem) =>
childItem.path !== undefined || getChildrenWithPages(childItem)?.length
)
childItem.type === "link" ||
(childItem.type !== "separator" &&
getChildrenWithPages(childItem)?.length)
) as SidebarItemWithParent[]
}
const getPrevItem = (
items: SidebarItemType[],
items: SidebarItem[],
index: number
): SidebarItemWithParent | undefined => {
let foundItem: SidebarItemWithParent | undefined
@@ -82,6 +84,9 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
.slice(0, index)
.reverse()
.some((item) => {
if (item.type === "separator") {
return false
}
if (item.children?.length) {
const childItem = getPrevItem(item.children, item.children.length)
if (childItem) {
@@ -90,7 +95,7 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
parent: item,
}
}
} else if (item.path) {
} else if (item.type === "link") {
foundItem = item
}
@@ -101,12 +106,16 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
}
const getNextItem = (
items: SidebarItemType[],
items: SidebarItem[],
index: number
): SidebarItemWithParent | undefined => {
let foundItem: SidebarItemWithParent | undefined
items.slice(index + 1).some((item) => {
if (item.path) {
if (item.type === "separator") {
return false
}
if (item.type === "link") {
foundItem = item
} else if (item.children?.length) {
const childItem = getNextItem(item.children, -1)
@@ -124,13 +133,13 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
return foundItem
}
const searchItems = (currentItems: SidebarItemType[]): SearchItemsResult => {
const searchItems = (currentItems: SidebarItem[]): SearchItemsResult => {
const result: SearchItemsResult = {
foundActive: false,
}
result.foundActive = currentItems.some((item, index) => {
if (item.path === activePath) {
if (item.type === "link" && item.path === activePath) {
if (index !== 0) {
result.prevItem = getPrevItem(currentItems, index)
}
@@ -145,16 +154,15 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
return true
}
if (item.children?.length) {
if (item.type !== "separator" && item.children?.length) {
const childrenResult = searchItems(item.children)
if (childrenResult.foundActive) {
result.prevItem = childrenResult.prevItem
result.nextItem = childrenResult.nextItem
if (!result.prevItem) {
result.prevItem = item.path
? item
: getPrevItem(currentItems, index)
result.prevItem =
item.type === "link" ? item : getPrevItem(currentItems, index)
}
if (!result.nextItem && index !== currentItems.length - 1) {
@@ -178,8 +186,11 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
result.prevItem
? {
title: result.prevItem.title,
link: result.prevItem.path || "",
parentTitle: result.prevItem.parent?.title,
link: result.prevItem.type === "link" ? result.prevItem.path : "",
parentTitle:
result.prevItem.parent?.type !== "separator"
? result.prevItem.parent?.title
: undefined,
}
: undefined
)
@@ -187,8 +198,11 @@ export const PaginationProvider = ({ children }: PaginationProviderProps) => {
result.nextItem
? {
title: result.nextItem.title,
link: result.nextItem.path || "",
parentTitle: result.nextItem.parent?.title,
link: result.nextItem.type === "link" ? result.nextItem.path : "",
parentTitle:
result.nextItem.parent?.type !== "separator"
? result.nextItem.parent?.title
: undefined,
}
: undefined
)
@@ -15,53 +15,47 @@ import { getScrolledTop } from "@/utils"
import { useIsBrowser } from "@/hooks"
import {
SidebarItemSections,
SidebarItemType,
SidebarSectionItemsType,
SidebarItem,
SidebarSectionItems,
SidebarItemLink,
InteractiveSidebarItem,
SidebarItemCategory,
} from "types"
export type CurrentItemsState = SidebarSectionItemsType & {
export type CurrentItemsState = SidebarSectionItems & {
previousSidebar?: CurrentItemsState
}
export type SidebarStyleOptions = {
disableActiveTransition?: boolean
noTitleStyling?: boolean
}
export type SidebarContextType = {
items: SidebarSectionItemsType
items: SidebarSectionItems
currentItems: CurrentItemsState | undefined
activePath: string | null
getActiveItem: () => SidebarItemType | undefined
getActiveItem: () => SidebarItemLink | undefined
setActivePath: (path: string | null) => void
isItemActive: (item: SidebarItemType, checkChildren?: boolean) => boolean
addItems: (
item: SidebarItemType[],
options?: {
section?: SidebarItemSections
parent?: {
path: string
changeLoaded?: boolean
}
indexPosition?: number
ignoreExisting?: boolean
}
) => void
isLinkActive: (item: SidebarItem, checkChildren?: boolean) => boolean
isChildrenActive: (item: SidebarItemCategory) => boolean
addItems: (item: SidebarItem[], options?: ActionOptionsType) => void
findItemInSection: (
section: SidebarItemType[],
item: Partial<SidebarItemType>,
section: SidebarItem[],
item: Partial<SidebarItem>,
checkChildren?: boolean
) => SidebarItemType | undefined
) => SidebarItem | undefined
mobileSidebarOpen: boolean
setMobileSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
isSidebarEmpty: () => boolean
desktopSidebarOpen: boolean
setDesktopSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>
staticSidebarItems?: boolean
shouldHandleHashChange: boolean
sidebarRef: React.RefObject<HTMLDivElement>
goBack: () => void
sidebarTopHeight: number
setSidebarTopHeight: React.Dispatch<React.SetStateAction<number>>
resetItems: () => void
isItemLoaded: (path: string) => boolean
} & SidebarStyleOptions
export const SidebarContext = createContext<SidebarContextType | null>(null)
@@ -70,6 +64,7 @@ export type ActionOptionsType = {
section?: SidebarItemSections
parent?: {
path: string
title: string
changeLoaded?: boolean
}
indexPosition?: number
@@ -79,25 +74,36 @@ export type ActionOptionsType = {
export type ActionType =
| {
type: "add" | "update"
items: SidebarItemType[]
items: SidebarItem[]
options?: ActionOptionsType
}
| {
type: "replace"
replacementItems: SidebarSectionItemsType
replacementItems: SidebarSectionItems
}
const areItemsEqual = (itemA: SidebarItem, itemB: SidebarItem): boolean => {
if (itemA.type === "separator" || itemB.type === "separator") {
return false
}
const hasSameTitle = itemA.title === itemB.title
const hasSamePath =
itemA.type === "link" && itemB.type === "link" && itemA.path === itemB.path
return hasSameTitle || hasSamePath
}
const findItem = (
section: SidebarItemType[],
item: Partial<SidebarItemType>,
section: SidebarItem[],
item: Partial<SidebarItem>,
checkChildren = true
): SidebarItemType | undefined => {
let foundItem: SidebarItemType | undefined
): SidebarItemLink | undefined => {
let foundItem: SidebarItemLink | undefined
section.some((i) => {
if (
(!item.path && !i.path && i.title === item.title) ||
i.path === item.path
) {
if (i.type === "separator") {
return false
}
if (areItemsEqual(item as SidebarItem, i) && i.type === "link") {
foundItem = i
} else if (checkChildren && i.children) {
foundItem = findItem(i.children, item)
@@ -109,28 +115,19 @@ const findItem = (
return foundItem
}
export const reducer = (
state: SidebarSectionItemsType,
actionData: ActionType
) => {
export const reducer = (state: SidebarSectionItems, actionData: ActionType) => {
if (actionData.type === "replace") {
return actionData.replacementItems
}
const { type, options } = actionData
let { items } = actionData
const {
section = SidebarItemSections.TOP,
parent,
ignoreExisting = false,
indexPosition,
} = options || {}
const { parent, ignoreExisting = false, indexPosition } = options || {}
const sectionName = SidebarItemSections.DEFAULT
const sectionItems = state[sectionName]
if (!ignoreExisting) {
const selectedSection =
section === SidebarItemSections.BOTTOM ? state.bottom : state.top
items = items.filter((item) => !findItem(selectedSection, item))
items = items.filter((item) => !findItem(sectionItems, item))
}
if (!items.length) {
@@ -141,21 +138,24 @@ export const reducer = (
case "add":
return {
...state,
[section]:
[sectionName]:
indexPosition !== undefined
? [
...state[section].slice(0, indexPosition),
...sectionItems.slice(0, indexPosition),
...items,
...state[section].slice(indexPosition),
...sectionItems.slice(indexPosition),
]
: [...state[section], ...items],
: [...sectionItems, ...items],
}
case "update":
// find item index
return {
...state,
[section]: state[section].map((i) => {
if (i.path && parent?.path && i.path === parent?.path) {
[sectionName]: sectionItems.map((i) => {
if (i.type === "separator") {
return i
}
if (parent && areItemsEqual(i, parent as SidebarItem)) {
return {
...i,
children:
@@ -166,7 +166,11 @@ export const reducer = (
...(i.children?.slice(indexPosition) || []),
]
: [...(i.children || []), ...items],
loaded: parent.changeLoaded ? true : i.loaded,
loaded: parent.changeLoaded
? true
: i.type === "link"
? i.loaded
: true,
}
}
return i
@@ -181,7 +185,7 @@ export type SidebarProviderProps = {
children?: ReactNode
isLoading?: boolean
setIsLoading?: React.Dispatch<React.SetStateAction<boolean>>
initialItems?: SidebarSectionItemsType
initialItems?: SidebarSectionItems
shouldHandleHashChange?: boolean
shouldHandlePathChange?: boolean
scrollableElement?: Element | Window
@@ -199,12 +203,10 @@ export const SidebarProvider = ({
scrollableElement,
staticSidebarItems = false,
disableActiveTransition = false,
noTitleStyling = false,
resetOnCondition,
}: SidebarProviderProps) => {
const [items, dispatch] = useReducer(reducer, {
top: initialItems?.top || [],
bottom: initialItems?.bottom || [],
default: initialItems?.default || [],
mobile: initialItems?.mobile || [],
})
const [currentItems, setCurrentItems] = useState<
@@ -212,6 +214,7 @@ export const SidebarProvider = ({
>()
const [activePath, setActivePath] = useState<string | null>("")
const [mobileSidebarOpen, setMobileSidebarOpen] = useState<boolean>(false)
const [sidebarTopHeight, setSidebarTopHeight] = useState(0)
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true)
const sidebarRef = useRef<HTMLDivElement>(null)
@@ -224,30 +227,29 @@ export const SidebarProvider = ({
const findItemInSection = useCallback(findItem, [])
const isItemLoaded = useCallback(
(path: string) => {
const item =
findItemInSection(items.mobile, { path, type: "link" }) ||
findItemInSection(items.default, { path, type: "link" })
return item?.loaded || false
},
[items]
)
const getActiveItem = useCallback(() => {
if (activePath === null) {
return undefined
}
return (
findItemInSection(items.mobile, { path: activePath }) ||
findItemInSection(items.top, { path: activePath }) ||
findItemInSection(items.bottom, { path: activePath })
findItemInSection(items.mobile, { path: activePath, type: "link" }) ||
findItemInSection(items.default, { path: activePath, type: "link" })
)
}, [activePath, items, findItemInSection])
const addItems = (
newItems: SidebarItemType[],
options?: {
section?: SidebarItemSections
parent?: {
path: string
changeLoaded?: boolean
}
indexPosition?: number
ignoreExisting?: boolean
}
) => {
const addItems = (newItems: SidebarItem[], options?: ActionOptionsType) => {
dispatch({
type: options?.parent ? "update" : "add",
items: newItems,
@@ -255,8 +257,12 @@ export const SidebarProvider = ({
})
}
const isItemActive = useCallback(
(item: SidebarItemType, checkChildren = false): boolean => {
const isLinkActive = useCallback(
(item: SidebarItem, checkChildren = false): boolean => {
if (item.type !== "link") {
return false
}
return (
item.path === activePath ||
(checkChildren && activePath?.split("_")[0] === item.path)
@@ -265,15 +271,22 @@ export const SidebarProvider = ({
[activePath]
)
const isSidebarEmpty = useCallback((): boolean => {
return Object.values(items).every((sectionItems) => {
if (!Array.isArray(sectionItems)) {
return true
}
const isChildrenActive = useCallback(
(item: InteractiveSidebarItem): boolean => {
return (
item.children?.some((child) => {
if (isLinkActive(child, true)) {
return true
}
return sectionItems.length === 0
})
}, [items])
return child.type !== "separator" && child.children
? isChildrenActive(child)
: false
}) || false
)
},
[isLinkActive]
)
const init = () => {
const currentPath = location.hash.replace("#", "")
@@ -283,23 +296,27 @@ export const SidebarProvider = ({
}
const getCurrentSidebar = useCallback(
(searchItems: SidebarItemType[]): SidebarItemType | undefined => {
let currentSidebar: SidebarItemType | undefined
(searchItems: SidebarItem[]): InteractiveSidebarItem | undefined => {
let currentSidebar: InteractiveSidebarItem | undefined
searchItems.some((item) => {
if (item.isChildSidebar) {
if (isItemActive(item)) {
currentSidebar = item
} else if (item.children?.length) {
const childSidebar =
getCurrentSidebar(item.children) ||
findItem(item.children, { path: activePath || undefined })
if (item.type === "separator") {
return false
}
if (item.isChildSidebar && isLinkActive(item)) {
currentSidebar = item
}
if (childSidebar) {
currentSidebar = childSidebar.isChildSidebar ? childSidebar : item
}
if (!currentSidebar && item.children?.length) {
const childSidebar =
getCurrentSidebar(item.children) ||
findItem(item.children, {
path: activePath || undefined,
type: "link",
})
if (childSidebar) {
currentSidebar = childSidebar.isChildSidebar ? childSidebar : item
}
} else if (item.children?.length) {
currentSidebar = getCurrentSidebar(item.children)
}
return currentSidebar !== undefined
@@ -307,7 +324,7 @@ export const SidebarProvider = ({
return currentSidebar
},
[isItemActive, activePath]
[isLinkActive, activePath]
)
const goBack = () => {
@@ -317,9 +334,9 @@ export const SidebarProvider = ({
const previousSidebar = currentItems.previousSidebar || items
const backItem =
previousSidebar.top.find((item) => item.path && !item.isChildSidebar) ||
previousSidebar.bottom.find((item) => item.path && !item.isChildSidebar)
const backItem = previousSidebar.default.find(
(item) => item.type === "link" && !item.isChildSidebar
) as SidebarItemLink
if (!backItem) {
return
@@ -334,8 +351,7 @@ export const SidebarProvider = ({
dispatch({
type: "replace",
replacementItems: {
top: initialItems?.top || [],
bottom: initialItems?.bottom || [],
default: initialItems?.default || [],
mobile: initialItems?.mobile || [],
},
})
@@ -391,7 +407,7 @@ export const SidebarProvider = ({
}, [shouldHandleHashChange, isBrowser])
useEffect(() => {
if (isLoading && items.top.length && items.bottom.length) {
if (isLoading && items.default.length) {
setIsLoading?.(false)
}
}, [items, isLoading, setIsLoading])
@@ -412,8 +428,7 @@ export const SidebarProvider = ({
return
}
const currentSidebar =
getCurrentSidebar(items.top) || getCurrentSidebar(items.bottom)
const currentSidebar = getCurrentSidebar(items.default)
if (!currentSidebar) {
setCurrentItems(undefined)
@@ -423,18 +438,20 @@ export const SidebarProvider = ({
if (
currentSidebar.isChildSidebar &&
currentSidebar.children &&
currentItems?.parentItem?.path !== currentSidebar.path
(!currentItems?.parentItem ||
!areItemsEqual(currentItems?.parentItem, currentSidebar))
) {
const { children, ...parentItem } = currentSidebar
const hasPreviousSidebar =
currentItems?.previousSidebar?.parentItem?.type === "link" &&
parentItem.type === "link" &&
currentItems.previousSidebar.parentItem.path !== parentItem.path
setCurrentItems({
top: children,
bottom: [],
default: children,
mobile: items.mobile,
parentItem: parentItem,
previousSidebar:
currentItems?.previousSidebar?.parentItem?.path !== parentItem.path
? currentItems
: undefined,
previousSidebar: hasPreviousSidebar ? currentItems : undefined,
})
}
}, [getCurrentSidebar, activePath])
@@ -443,7 +460,7 @@ export const SidebarProvider = ({
if (resetOnCondition?.()) {
resetItems()
}
}, [resetOnCondition])
}, [resetOnCondition, resetItems])
return (
<SidebarContext.Provider
@@ -453,21 +470,23 @@ export const SidebarProvider = ({
addItems,
activePath,
setActivePath,
isItemActive,
isLinkActive: isLinkActive,
isChildrenActive: isChildrenActive,
findItemInSection,
mobileSidebarOpen,
setMobileSidebarOpen,
isSidebarEmpty,
getActiveItem,
desktopSidebarOpen,
setDesktopSidebarOpen,
getActiveItem,
staticSidebarItems,
disableActiveTransition,
noTitleStyling,
shouldHandleHashChange,
sidebarRef,
goBack,
sidebarTopHeight,
setSidebarTopHeight,
resetItems,
isItemLoaded,
}}
>
{children}
@@ -23,8 +23,7 @@ export const SiteConfigProvider = ({
initConfig || {
baseUrl: "",
sidebar: {
top: [],
bottom: [],
default: [],
mobile: [],
},
}
+1 -1
View File
@@ -2,9 +2,9 @@ export * from "./AiAssistant"
export * from "./Analytics"
export * from "./ColorMode"
export * from "./LearningPath"
export * from "./MainNav"
export * from "./Mobile"
export * from "./Modal"
export * from "./Navbar"
export * from "./Notification"
export * from "./PageLoading"
export * from "./Pagination"