Skip to content

Commit 019dfd2

Browse files
committed
Add collapsible docs sidebar UI
Enable hideable docs sidebar in docusaurus.config and add a full collapsible sidebar implementation. Introduces a SidebarCollapseContext and new theme components for the DocRoot sidebar and desktop sidebar/content (including filter, expand-all toggle, collapse/expand buttons, and announcement-bar handling), plus corresponding CSS modules. Files added: src/contexts/SidebarCollapseContext.tsx, src/theme/DocRoot/Layout/Sidebar/{index.tsx,styles.module.css}, src/theme/DocSidebar/Desktop/{index.tsx,Content/index.tsx,Content/styles.module.css,styles.module.css}. This change centralizes collapse/expand behavior and adds filtering and UX polish for desktop doc sidebars.
1 parent bd85e46 commit 019dfd2

8 files changed

Lines changed: 718 additions & 0 deletions

File tree

β€Ždocusaurus.config.tsβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ const config: Config = {
176176
disableSwitch: false,
177177
respectPrefersColorScheme: true,
178178
},
179+
docs: {
180+
sidebar: {
181+
hideable: true,
182+
autoCollapseCategories: true,
183+
},
184+
},
179185
} satisfies Preset.ThemeConfig,
180186
};
181187

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { createContext, useContext, type ReactNode } from "react";
2+
3+
type SidebarCollapseContextValue = {
4+
onCollapse: () => void;
5+
};
6+
7+
const SidebarCollapseContext = createContext<SidebarCollapseContextValue>({
8+
onCollapse: () => {},
9+
});
10+
11+
export function SidebarCollapseProvider({
12+
onCollapse,
13+
children,
14+
}: {
15+
onCollapse: () => void;
16+
children: ReactNode;
17+
}) {
18+
return (
19+
<SidebarCollapseContext.Provider value={{ onCollapse }}>
20+
{children}
21+
</SidebarCollapseContext.Provider>
22+
);
23+
}
24+
25+
export function useSidebarCollapse() {
26+
return useContext(SidebarCollapseContext);
27+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { type ReactNode, useState, useCallback } from "react";
2+
import clsx from "clsx";
3+
import {
4+
prefersReducedMotion,
5+
ThemeClassNames,
6+
} from "@docusaurus/theme-common";
7+
import { useDocsSidebar } from "@docusaurus/plugin-content-docs/client";
8+
import { useLocation } from "@docusaurus/router";
9+
import DocSidebar from "@theme/DocSidebar";
10+
import type { Props } from "@theme/DocRoot/Layout/Sidebar";
11+
import { SidebarCollapseProvider } from "@site/src/contexts/SidebarCollapseContext";
12+
13+
import styles from "./styles.module.css";
14+
15+
function ResetOnSidebarChange({ children }: { children: ReactNode }) {
16+
const sidebar = useDocsSidebar();
17+
return (
18+
<React.Fragment key={sidebar?.name ?? "noSidebar"}>
19+
{children}
20+
</React.Fragment>
21+
);
22+
}
23+
24+
function ToggleChevron() {
25+
return (
26+
<svg
27+
width="16"
28+
height="16"
29+
viewBox="0 0 24 24"
30+
fill="none"
31+
stroke="currentColor"
32+
strokeWidth="2"
33+
strokeLinecap="round"
34+
strokeLinejoin="round"
35+
>
36+
<polyline points="9 18 15 12 9 6" />
37+
</svg>
38+
);
39+
}
40+
41+
export default function DocRootLayoutSidebar({
42+
sidebar,
43+
hiddenSidebarContainer,
44+
setHiddenSidebarContainer,
45+
}: Props): ReactNode {
46+
const { pathname } = useLocation();
47+
48+
const [hiddenSidebar, setHiddenSidebar] = useState(false);
49+
const toggleSidebar = useCallback(() => {
50+
if (hiddenSidebar) {
51+
setHiddenSidebar(false);
52+
}
53+
if (!hiddenSidebar && prefersReducedMotion()) {
54+
setHiddenSidebar(true);
55+
}
56+
setHiddenSidebarContainer((value: boolean) => !value);
57+
}, [setHiddenSidebarContainer, hiddenSidebar]);
58+
59+
return (
60+
<aside
61+
className={clsx(
62+
ThemeClassNames.docs.docSidebarContainer,
63+
styles.docSidebarContainer,
64+
hiddenSidebarContainer && styles.docSidebarContainerHidden
65+
)}
66+
onTransitionEnd={(e) => {
67+
if (
68+
!e.currentTarget.classList.contains(styles.docSidebarContainer!)
69+
) {
70+
return;
71+
}
72+
if (hiddenSidebarContainer) {
73+
setHiddenSidebar(true);
74+
}
75+
}}
76+
>
77+
<ResetOnSidebarChange>
78+
<div
79+
className={clsx(
80+
styles.sidebarViewport,
81+
hiddenSidebar && styles.sidebarViewportHidden
82+
)}
83+
>
84+
{/* ── Expanded: sidebar with collapse passed via context ── */}
85+
{!hiddenSidebar && (
86+
<SidebarCollapseProvider onCollapse={toggleSidebar}>
87+
<DocSidebar
88+
sidebar={sidebar}
89+
path={pathname}
90+
onCollapse={toggleSidebar}
91+
isHidden={hiddenSidebar}
92+
/>
93+
</SidebarCollapseProvider>
94+
)}
95+
96+
{/* ── Collapsed: just the expand button ── */}
97+
{hiddenSidebar && (
98+
<div className={styles.collapsedBar}>
99+
<button
100+
type="button"
101+
className={styles.toggleBtn}
102+
onClick={toggleSidebar}
103+
title="Expand sidebar"
104+
aria-label="Expand sidebar"
105+
>
106+
<ToggleChevron />
107+
</button>
108+
</div>
109+
)}
110+
</div>
111+
</ResetOnSidebarChange>
112+
</aside>
113+
);
114+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
:root {
2+
--doc-sidebar-width: 300px;
3+
--doc-sidebar-hidden-width: 44px;
4+
}
5+
6+
.docSidebarContainer {
7+
display: none;
8+
}
9+
10+
@media (min-width: 997px) {
11+
.docSidebarContainer {
12+
display: block;
13+
width: var(--doc-sidebar-width);
14+
margin-top: calc(-1 * var(--ifm-navbar-height));
15+
border-right: 1px solid var(--ifm-toc-border-color);
16+
will-change: width;
17+
transition: width var(--ifm-transition-fast) ease;
18+
clip-path: inset(0);
19+
}
20+
21+
.docSidebarContainerHidden {
22+
width: var(--doc-sidebar-hidden-width);
23+
cursor: pointer;
24+
border-right: none;
25+
}
26+
27+
.sidebarViewport {
28+
top: 0;
29+
position: sticky;
30+
height: 100%;
31+
max-height: 100vh;
32+
}
33+
}
34+
35+
/* ── Shared toggle button style (same as TOC) ── */
36+
.toggleBtn {
37+
display: flex;
38+
align-items: center;
39+
justify-content: center;
40+
width: 28px;
41+
height: 28px;
42+
border: 1px solid var(--ifm-toc-border-color);
43+
border-radius: 0.375rem;
44+
background: var(--ifm-background-surface-color, var(--ifm-background-color));
45+
cursor: pointer;
46+
color: var(--ifm-color-emphasis-600);
47+
transition: color 150ms ease, border-color 150ms ease;
48+
flex-shrink: 0;
49+
}
50+
51+
.toggleBtn:hover {
52+
color: var(--ifm-color-primary);
53+
border-color: var(--ifm-color-primary);
54+
}
55+
56+
/* ── Collapsed bar: just the toggle button centered ── */
57+
.collapsedBar {
58+
display: flex;
59+
justify-content: center;
60+
padding-top: calc(var(--ifm-navbar-height) + 0.75rem);
61+
}
62+
63+
@media (max-width: 996px) {
64+
.collapsedBar {
65+
display: none;
66+
}
67+
}

0 commit comments

Comments
Β (0)