Skip to content

Commit 87085e7

Browse files
RDoc-3539 Add sticky topbar with LanguageSwitcher
1 parent 25b2865 commit 87085e7

File tree

3 files changed

+220
-56
lines changed

3 files changed

+220
-56
lines changed

src/components/DocsTopbar.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React, { useState, useEffect } from "react";
2+
import clsx from "clsx";
3+
import LanguageSwitcher from "@site/src/components/LanguageSwitcher";
4+
5+
type DocsTopbarProps = {
6+
title: string;
7+
supportedLanguages?: string[];
8+
};
9+
10+
export default function DocsTopbar({
11+
title,
12+
supportedLanguages,
13+
}: DocsTopbarProps) {
14+
const [isVisible, setIsVisible] = useState(false);
15+
const [isCollapsed, setIsCollapsed] = useState(false);
16+
17+
useEffect(() => {
18+
const handleScroll = () => {
19+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
20+
const scrollThreshold = 250;
21+
setIsVisible(scrollTop >= scrollThreshold);
22+
};
23+
24+
window.addEventListener("scroll", handleScroll);
25+
handleScroll();
26+
27+
return () => {
28+
window.removeEventListener("scroll", handleScroll);
29+
};
30+
}, []);
31+
32+
useEffect(() => {
33+
if (window.innerWidth < 768) {
34+
setIsCollapsed(true);
35+
}
36+
}, []);
37+
38+
if (!supportedLanguages || supportedLanguages.length === 0) {
39+
return null;
40+
}
41+
42+
return (
43+
<div
44+
className={clsx(
45+
"sticky top-[71.46px] z-30",
46+
"rounded-xl",
47+
"transition-all duration-300 ease-in-out",
48+
{
49+
"max-h-[60px] opacity-100": isVisible,
50+
"max-h-0 opacity-0": !isVisible,
51+
},
52+
)}
53+
>
54+
<div className="row">
55+
<div className="col min-[1640px]:!p-0">
56+
<div
57+
className={clsx(
58+
"w-full p-2",
59+
"flex justify-between flex-wrap items-center",
60+
"rounded-xl",
61+
"border border-black/10 dark:border-white/10",
62+
"backdrop-blur supports-[backdrop-filter]:bg-white/90 dark:supports-[backdrop-filter]:bg-[#1b1b1d]/40 bg-white/95 dark:bg-[#1b1b1d]/90",
63+
"shadow-xl/30",
64+
"transition-all duration-300 ease-in-out",
65+
{
66+
"gap-2": !isCollapsed,
67+
},
68+
)}
69+
>
70+
<div className="flex justify-between items-center gap-2 truncate">
71+
<div
72+
className="text-base font-medium truncate"
73+
title={title}
74+
>
75+
{title}
76+
</div>
77+
<button
78+
className={clsx(
79+
"md:hidden ms-auto",
80+
"p-1 rounded",
81+
"hover:bg-black/5 dark:hover:bg-white/5",
82+
"transition-colors",
83+
)}
84+
onClick={() => setIsCollapsed(!isCollapsed)}
85+
aria-label={
86+
isCollapsed
87+
? "Show language switcher"
88+
: "Hide language switcher"
89+
}
90+
>
91+
<svg
92+
className={clsx(
93+
"w-4 h-4",
94+
"transition-transform duration-200",
95+
{
96+
"rotate-180": isCollapsed,
97+
},
98+
)}
99+
fill="none"
100+
stroke="currentColor"
101+
viewBox="0 0 24 24"
102+
>
103+
<path
104+
strokeLinecap="round"
105+
strokeLinejoin="round"
106+
strokeWidth={2}
107+
d="M19 9l-7 7-7-7"
108+
/>
109+
</svg>
110+
</button>
111+
</div>
112+
<div className="hidden md:block">
113+
<LanguageSwitcher
114+
supportedLanguages={supportedLanguages}
115+
flush
116+
/>
117+
</div>
118+
<div
119+
className={clsx(
120+
"md:hidden",
121+
"transition-all duration-300 ease-in-out",
122+
{
123+
"max-h-0 opacity-0 overflow-hidden":
124+
isCollapsed,
125+
"opacity-100": !isCollapsed,
126+
},
127+
)}
128+
>
129+
<LanguageSwitcher
130+
supportedLanguages={supportedLanguages}
131+
flush
132+
/>
133+
</div>
134+
</div>
135+
</div>
136+
<div className="col col--3 lg:block"></div>
137+
</div>
138+
</div>
139+
);
140+
}

src/components/LanguageSwitcher.tsx

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,61 @@ import { useLanguage } from "./LanguageContext";
33
import clsx from "clsx";
44

55
const languages = [
6-
{ label: "C#", value: "csharp", brand: "#9179E4" },
7-
{ label: "Java", value: "java", brand: "#f89820" },
8-
{ label: "Python", value: "python", brand: "#fbcb24" },
9-
{ label: "PHP", value: "php", brand: "#8993be" },
10-
{ label: "Node.JS", value: "nodejs", brand: "#3c873a" },
6+
{ label: "C#", value: "csharp", brand: "#9179E4" },
7+
{ label: "Java", value: "java", brand: "#f89820" },
8+
{ label: "Python", value: "python", brand: "#fbcb24" },
9+
{ label: "PHP", value: "php", brand: "#8993be" },
10+
{ label: "Node.JS", value: "nodejs", brand: "#3c873a" },
1111
];
1212

1313
type LanguageSwitcherProps = {
14-
supportedLanguages: string[];
14+
supportedLanguages: string[];
15+
flush?: boolean;
1516
};
1617

1718
export default function LanguageSwitcher({
18-
supportedLanguages,
19+
supportedLanguages,
20+
flush = false,
1921
}: LanguageSwitcherProps) {
20-
const { language, setLanguage } = useLanguage();
22+
const { language, setLanguage } = useLanguage();
2123

22-
useEffect(() => {
23-
if (!supportedLanguages.includes(language)) {
24-
setLanguage(supportedLanguages[0]);
25-
}
26-
}, [supportedLanguages, language, setLanguage]);
24+
useEffect(() => {
25+
if (!supportedLanguages.includes(language)) {
26+
setLanguage(supportedLanguages[0]);
27+
}
28+
}, [supportedLanguages, language, setLanguage]);
2729

28-
return (
29-
<div className="flex flex-wrap gap-2 mb-8">
30-
{languages
31-
.filter((lang) => supportedLanguages.includes(lang.value))
32-
.map((lang) => {
33-
const isActive = language === lang.value;
30+
return (
31+
<div className={`flex flex-wrap gap-2 ${flush ? '' : 'mb-8'}`}>
32+
{languages
33+
.filter((lang) => supportedLanguages.includes(lang.value))
34+
.map((lang) => {
35+
const isActive = language === lang.value;
3436

35-
return (
36-
<button
37-
key={lang.value}
38-
type="button"
39-
onClick={() => setLanguage(lang.value)}
40-
className={clsx(
41-
"px-3 py-1.5 rounded-md border text-sm transition-colors cursor-pointer",
42-
"border-gray-300 text-gray-500 hover:bg-black/5 hover:border-gray-500 hover:text-gray-600",
43-
"dark:text-gray-300 dark:border-gray-600 dark:hover:text-gray-200 dark:hover:border-gray-400 dark:hover:bg-white/5",
44-
)}
45-
style={
46-
isActive
47-
? {
48-
backgroundColor: `${lang.brand}20`,
49-
color: lang.brand,
50-
borderColor: lang.brand,
51-
}
52-
: {}
53-
}
54-
>
55-
{lang.label}
56-
</button>
57-
);
58-
})}
59-
</div>
60-
);
37+
return (
38+
<button
39+
key={lang.value}
40+
type="button"
41+
onClick={() => setLanguage(lang.value)}
42+
className={clsx(
43+
"px-3 py-1.5 rounded-md border text-sm transition-colors cursor-pointer",
44+
"border-gray-300 text-gray-500 hover:bg-black/5 hover:border-gray-500 hover:text-gray-600",
45+
"dark:text-gray-300 dark:border-gray-600 dark:hover:text-gray-200 dark:hover:border-gray-400 dark:hover:bg-white/5",
46+
)}
47+
style={
48+
isActive
49+
? {
50+
backgroundColor: `${lang.brand}20`,
51+
color: lang.brand,
52+
borderColor: lang.brand,
53+
}
54+
: {}
55+
}
56+
>
57+
{lang.label}
58+
</button>
59+
);
60+
})}
61+
</div>
62+
);
6163
}

src/theme/DocItem/index.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,43 @@ import React, { type ReactNode } from "react";
22
import DocItem from "@theme-original/DocItem";
33
import type DocItemType from "@theme/DocItem";
44
import type { WrapperProps } from "@docusaurus/types";
5+
import DocsTopbar from "@site/src/components/DocsTopbar";
56

67
type Props = WrapperProps<typeof DocItemType>;
78

89
export default function DocItemWrapper(props: Props): ReactNode {
9-
const title = props.content.metadata?.title;
10-
const isHomePage =
11-
title === "RavenDB Documentation" ||
12-
title === "RavenDB Cloud Documentation";
10+
const title = props.content.metadata?.title;
11+
const source = props.content.metadata?.source as string | undefined;
12+
13+
const isDocsOrVersioned =
14+
source?.startsWith("@site/docs/") ||
15+
source?.startsWith("@site/versioned_docs/") ||
16+
source?.startsWith("docs/") ||
17+
source?.startsWith("versioned_docs/");
18+
const fileName = source?.split("/").pop();
19+
const isExcluded = fileName === "home.mdx" || fileName === "whats-new.mdx";
20+
21+
const supportedLanguages =
22+
(props.content as any)?.supportedLanguages ||
23+
(props.content as any)?.frontMatter?.supportedLanguages ||
24+
(props.content as any)?.exports?.supportedLanguages;
25+
26+
const showTopbar = Boolean(isDocsOrVersioned && !isExcluded);
1327

1428
return (
15-
<div className="wrapper row">
16-
<div className="col flex-1 min-w-0">
17-
<DocItem {...props} />
18-
</div>
19-
{!isHomePage && <div className="col col--3 lg:block"></div>}
20-
</div>
21-
);
29+
<>
30+
{showTopbar && (
31+
<DocsTopbar
32+
title={title}
33+
supportedLanguages={supportedLanguages}
34+
/>
35+
)}
36+
<div className="wrapper row">
37+
<div className="col flex-1 min-w-0">
38+
<DocItem {...props} />
39+
</div>
40+
<div className="col col--3 lg:block"></div>
41+
</div>
42+
</>
43+
);
2244
}

0 commit comments

Comments
 (0)