diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx
new file mode 100644
index 00000000..06ce934b
--- /dev/null
+++ b/src/theme/TOC/index.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useState, useCallback, useRef } from "react";
+import clsx from "clsx";
+import { useLocation } from "@docusaurus/router";
+import TOCItems from "@theme/TOCItems";
+import type { Props } from "@theme/TOC";
+
+import styles from "./styles.module.css";
+
+const LINK_CLASS_NAME = "table-of-contents__link toc-highlight";
+const LINK_ACTIVE_CLASS_NAME = "table-of-contents__link--active";
+
+function ReadingProgress(): JSX.Element {
+ const [progress, setProgress] = useState(0);
+ const rafRef = useRef
(null);
+
+ const computeProgress = useCallback(() => {
+ const scrollY = window.scrollY;
+ const winHeight = window.innerHeight;
+ const docHeight = document.documentElement.scrollHeight;
+ const maxScroll = docHeight - winHeight;
+ const percent =
+ maxScroll > 0 ? Math.min(100, Math.max(0, (scrollY / maxScroll) * 100)) : 0;
+ setProgress(Math.round(percent));
+ }, []);
+
+ const handleScroll = useCallback(() => {
+ if (rafRef.current !== null) return;
+ rafRef.current = requestAnimationFrame(() => {
+ rafRef.current = null;
+ computeProgress();
+ });
+ }, [computeProgress]);
+
+ useEffect(() => {
+ window.addEventListener("scroll", handleScroll, { passive: true });
+ computeProgress();
+ return () => {
+ window.removeEventListener("scroll", handleScroll);
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
+ };
+ }, [handleScroll, computeProgress]);
+
+ return (
+
+
+
+
+ Reading Progress
+
+
{progress}%
+
+
+
+ );
+}
+
+export default function TOC({ className, ...props }: Props): JSX.Element {
+ const { pathname } = useLocation();
+ const isBlogPost = pathname.includes("/blog/") && pathname !== "/blog/";
+
+ return (
+
+ {isBlogPost && }
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css
new file mode 100644
index 00000000..0982da29
--- /dev/null
+++ b/src/theme/TOC/styles.module.css
@@ -0,0 +1,64 @@
+.tableOfContents {
+ max-height: calc(100vh - (var(--ifm-navbar-height) + 2rem));
+ overflow-y: auto;
+ position: sticky;
+ top: calc(var(--ifm-navbar-height) + 1rem);
+ border: 1px solid var(--ifm-toc-border-color);
+ border-radius: 10px;
+ background: var(--ifm-background-surface-color);
+ padding: 12px;
+}
+
+@media (max-width: 996px) {
+ .tableOfContents {
+ display: none;
+ }
+}
+
+.progressContainer {
+ padding-bottom: 10px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid var(--ifm-toc-border-color);
+}
+
+.progressHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.progressLabel {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--ifm-font-color-base);
+}
+
+.progressIcon {
+ flex-shrink: 0;
+ color: var(--ifm-color-primary);
+}
+
+.progressPercent {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--ifm-color-emphasis-600);
+}
+
+.progressTrack {
+ width: 100%;
+ height: 6px;
+ border-radius: 999px;
+ background: var(--ifm-color-emphasis-200);
+ overflow: hidden;
+}
+
+.progressFill {
+ height: 100%;
+ border-radius: 999px;
+ background: var(--ifm-color-primary);
+ transition: width 0.1s linear;
+}
\ No newline at end of file