Skip to content

Commit a18a17d

Browse files
devin-ai-integration[bot]harshika@hyprnote.comComputelessComputer
authored
blog: add table of contents to left sidebar (#3866)
* blog: add table of contents to left sidebar Co-Authored-By: [email protected] <[email protected]> * refactor(blog): improve table of contents with active section tracking * feat(blog): improve table of contents navigation scroll behavior * feat(blog): improve table of contents with context and interactivity * style(css): adjust scroll margin for blog article headers * feat(ui): add responsive height and visibility adjustments * refactor(header): adjust responsive breakpoint for Why Hyprnote link --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]> Co-authored-by: ComputelessComputer <[email protected]>
1 parent b694af8 commit a18a17d

File tree

5 files changed

+328
-75
lines changed

5 files changed

+328
-75
lines changed

apps/web/src/components/header.tsx

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
BookOpen,
44
Building2,
55
ChevronDown,
6+
ChevronLeft,
7+
ChevronRight,
68
ChevronUp,
79
FileText,
810
History,
@@ -20,6 +22,7 @@ import { useEffect, useRef, useState } from "react";
2022
import { cn } from "@hypr/utils";
2123

2224
import { SearchTrigger } from "@/components/search";
25+
import { useBlogToc } from "@/hooks/use-blog-toc";
2326
import { useDocsDrawer } from "@/hooks/use-docs-drawer";
2427
import { useHandbookDrawer } from "@/hooks/use-handbook-drawer";
2528
import { getPlatformCTA, usePlatform } from "@/hooks/use-platform";
@@ -88,8 +91,12 @@ export function Header() {
8891
const isDocsPage = router.location.pathname.startsWith("/docs");
8992
const isHandbookPage =
9093
router.location.pathname.startsWith("/company-handbook");
94+
const isBlogArticlePage =
95+
router.location.pathname.startsWith("/blog/") &&
96+
router.location.pathname !== "/blog/";
9197
const docsDrawer = useDocsDrawer();
9298
const handbookDrawer = useHandbookDrawer();
99+
const blogToc = useBlogToc();
93100
const lastScrollY = useRef(0);
94101

95102
useEffect(() => {
@@ -161,14 +168,19 @@ export function Header() {
161168
<SearchTrigger variant="mobile" />
162169
</div>
163170
)}
171+
{isBlogArticlePage && blogToc && blogToc.toc.length > 0 && (
172+
<BlogTocSubBar blogToc={blogToc} maxWidthClass={maxWidthClass} />
173+
)}
164174
</header>
165175

166176
{/* Spacer to account for fixed header */}
167177
<div
168178
className={
169179
isDocsPage || isHandbookPage
170180
? "h-17.25 md:h-17.25 max-md:h-[calc(69px+52px)]"
171-
: "h-17.25"
181+
: isBlogArticlePage && blogToc && blogToc.toc.length > 0
182+
? "h-[calc(69px+44px)] sm:h-17.25"
183+
: "h-17.25"
172184
}
173185
/>
174186

@@ -220,7 +232,7 @@ function LeftNav({
220232
<Logo />
221233
<Link
222234
to="/why-hyprnote/"
223-
className="hidden sm:block text-sm text-neutral-600 hover:text-neutral-800 transition-all hover:underline decoration-dotted"
235+
className="hidden md:block text-sm text-neutral-600 hover:text-neutral-800 transition-all hover:underline decoration-dotted"
224236
>
225237
Why Hyprnote
226238
</Link>
@@ -788,6 +800,71 @@ function MobileSolutionsList({
788800
);
789801
}
790802

803+
function BlogTocSubBar({
804+
blogToc,
805+
maxWidthClass,
806+
}: {
807+
blogToc: NonNullable<ReturnType<typeof useBlogToc>>;
808+
maxWidthClass: string;
809+
}) {
810+
const { toc, activeId, scrollToHeading } = blogToc;
811+
const activeIndex = toc.findIndex((item) => item.id === activeId);
812+
const activeItem = activeIndex >= 0 ? toc[activeIndex] : toc[0];
813+
814+
const goPrev = () => {
815+
const prevIndex = Math.max(0, activeIndex - 1);
816+
scrollToHeading(toc[prevIndex].id);
817+
};
818+
819+
const goNext = () => {
820+
const nextIndex = Math.min(toc.length - 1, activeIndex + 1);
821+
scrollToHeading(toc[nextIndex].id);
822+
};
823+
824+
return (
825+
<div
826+
className={`${maxWidthClass} mx-auto border-x border-neutral-100 border-t border-t-neutral-50 sm:hidden`}
827+
>
828+
<div className="flex items-center h-11 px-2">
829+
<button
830+
onClick={goPrev}
831+
disabled={activeIndex <= 0}
832+
className={cn([
833+
"shrink-0 p-1.5 rounded-md transition-colors cursor-pointer",
834+
activeIndex <= 0
835+
? "text-neutral-200"
836+
: "text-neutral-500 hover:text-stone-700 hover:bg-stone-50",
837+
])}
838+
>
839+
<ChevronLeft size={14} />
840+
</button>
841+
<button
842+
onClick={() => {
843+
if (activeItem) scrollToHeading(activeItem.id);
844+
}}
845+
className="flex-1 min-w-0 px-2 cursor-pointer"
846+
>
847+
<p className="text-sm text-stone-700 font-medium truncate text-center">
848+
{activeItem?.text}
849+
</p>
850+
</button>
851+
<button
852+
onClick={goNext}
853+
disabled={activeIndex >= toc.length - 1}
854+
className={cn([
855+
"shrink-0 p-1.5 rounded-md transition-colors cursor-pointer",
856+
activeIndex >= toc.length - 1
857+
? "text-neutral-200"
858+
: "text-neutral-500 hover:text-stone-700 hover:bg-stone-50",
859+
])}
860+
>
861+
<ChevronRight size={14} />
862+
</button>
863+
</div>
864+
</div>
865+
);
866+
}
867+
791868
function MobileMenuCTAs({
792869
platform,
793870
platformCTA,

apps/web/src/hooks/use-blog-toc.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createContext, useContext } from "react";
2+
3+
interface BlogTocContextType {
4+
toc: Array<{ id: string; text: string; level: number }>;
5+
activeId: string | null;
6+
setToc: (toc: Array<{ id: string; text: string; level: number }>) => void;
7+
setActiveId: (id: string | null) => void;
8+
scrollToHeading: (id: string) => void;
9+
}
10+
11+
export const BlogTocContext = createContext<BlogTocContextType | null>(null);
12+
13+
export function useBlogToc() {
14+
return useContext(BlogTocContext);
15+
}

0 commit comments

Comments
 (0)