Skip to content

Commit 29408a9

Browse files
committed
Refine docs top bar and sidebar layout
1 parent 97454ac commit 29408a9

10 files changed

Lines changed: 611 additions & 131 deletions

File tree

apps/web/src/app/(lander-docs)/components/docs/docs-sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function DocsSidebar({
1212

1313
return (
1414
<Sidebar
15-
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--header-height)-var(--footer-height))] bg-transparent lg:flex"
15+
className="sticky top-[calc(var(--header-height)+var(--docs-topbar-height)+1px)] z-30 hidden h-[calc(100svh-var(--header-height)-var(--docs-topbar-height)-var(--footer-height))] self-start bg-transparent lg:flex"
1616
collapsible="none"
1717
{...props}
1818
>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { Badge } from "@/components/ui/badge";
5+
import { Button } from "@/components/ui/button";
6+
import Icon from "@/components/ui/icons";
7+
import type { LatestRelease } from "@/lib/latest-release";
8+
import { cn } from "@/lib/utils";
9+
import { FullWidthBorder } from "../full-width-border";
10+
import { LLMCopyButton, ViewOptions } from "../page-actions";
11+
12+
type DocsTopBarClientProps = {
13+
widgetVersion: string;
14+
markdownUrl: string;
15+
githubUrl: string;
16+
latestRelease?: LatestRelease | null;
17+
previous?: {
18+
url: string;
19+
name?: React.ReactNode;
20+
} | null;
21+
next?: {
22+
url: string;
23+
name?: React.ReactNode;
24+
} | null;
25+
};
26+
27+
export function DocsTopBarClient({
28+
widgetVersion,
29+
markdownUrl,
30+
githubUrl,
31+
latestRelease,
32+
previous,
33+
next,
34+
}: DocsTopBarClientProps) {
35+
return (
36+
<div
37+
className="fixed top-[68px] right-0 left-0 z-40"
38+
data-slot="docs-topbar"
39+
>
40+
<div className="fixed top-[68px] right-0 left-0 h-(--docs-topbar-height) min-w-screen bg-background/95 backdrop-blur-sm" />
41+
<div className="container-wrapper relative mx-auto bg-background/95 backdrop-blur-sm">
42+
<div className="container relative flex h-(--docs-topbar-height) min-w-0 items-center justify-between gap-3 px-4 lg:px-6">
43+
<div className="flex min-w-0 items-center gap-2">
44+
<Badge
45+
className="shrink-0 rounded px-2 py-1 font-mono text-[11px] leading-none"
46+
variant="secondary"
47+
>
48+
Widget v{widgetVersion}
49+
</Badge>
50+
{latestRelease ? (
51+
<Link
52+
className="min-w-0 truncate px-1 py-0.5 font-mono text-primary/80 text-xs transition-colors hover:bg-background-300 hover:text-primary"
53+
data-slot="docs-topbar-whats-new"
54+
href="/changelog"
55+
>
56+
NEW: {latestRelease.tinyExcerpt}
57+
</Link>
58+
) : null}
59+
</div>
60+
<div
61+
className={cn(
62+
"no-scrollbar flex shrink-0 items-center gap-1 overflow-x-auto",
63+
"[&_a]:shrink-0 [&_button]:shrink-0"
64+
)}
65+
data-slot="docs-topbar-actions"
66+
>
67+
{previous ? (
68+
<Button
69+
asChild
70+
className="extend-touch-target size-8 shadow-none md:size-7"
71+
size="icon"
72+
variant="secondary"
73+
>
74+
<Link href={previous.url}>
75+
<Icon name="arrow-left" />
76+
<span className="sr-only">Previous</span>
77+
</Link>
78+
</Button>
79+
) : null}
80+
{next ? (
81+
<Button
82+
asChild
83+
className="extend-touch-target size-8 shadow-none md:size-7"
84+
size="icon"
85+
variant="secondary"
86+
>
87+
<Link href={next.url}>
88+
<Icon name="arrow-right" />
89+
<span className="sr-only">Next</span>
90+
</Link>
91+
</Button>
92+
) : null}
93+
<LLMCopyButton markdownUrl={markdownUrl} />
94+
<ViewOptions githubUrl={githubUrl} markdownUrl={markdownUrl} />
95+
</div>
96+
<FullWidthBorder className="bottom-0" />
97+
</div>
98+
</div>
99+
</div>
100+
);
101+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, expect, it, mock } from "bun:test";
2+
import type React from "react";
3+
import { renderToStaticMarkup } from "react-dom/server";
4+
5+
mock.module("next/link", () => ({
6+
default: ({
7+
children,
8+
href,
9+
...props
10+
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
11+
children: React.ReactNode;
12+
href: string;
13+
}) => (
14+
<a href={href} {...props}>
15+
{children}
16+
</a>
17+
),
18+
}));
19+
20+
mock.module("../page-actions", () => ({
21+
LLMCopyButton: ({ markdownUrl }: { markdownUrl: string }) => (
22+
<button
23+
data-markdown-url={markdownUrl}
24+
data-slot="mock-copy-page"
25+
type="button"
26+
/>
27+
),
28+
ViewOptions: ({
29+
githubUrl,
30+
markdownUrl,
31+
}: {
32+
githubUrl: string;
33+
markdownUrl: string;
34+
}) => (
35+
<button
36+
data-github-url={githubUrl}
37+
data-markdown-url={markdownUrl}
38+
data-slot="mock-view-options"
39+
type="button"
40+
/>
41+
),
42+
}));
43+
44+
mock.module("@/components/ui/button", () => ({
45+
Button: ({
46+
asChild,
47+
children,
48+
...props
49+
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
50+
asChild?: boolean;
51+
children: React.ReactNode;
52+
}) =>
53+
asChild ? (
54+
children
55+
) : (
56+
<button {...props} type={props.type ?? "button"}>
57+
{children}
58+
</button>
59+
),
60+
}));
61+
62+
mock.module("@/components/ui/badge", () => ({
63+
Badge: ({
64+
children,
65+
...props
66+
}: React.HTMLAttributes<HTMLSpanElement> & {
67+
children: React.ReactNode;
68+
}) => <span {...props}>{children}</span>,
69+
}));
70+
71+
mock.module("@/components/ui/icons", () => ({
72+
__esModule: true,
73+
default: ({ name }: { name: string }) => <span data-icon={name} />,
74+
}));
75+
76+
const modulePromise = import("./docs-topbar.client");
77+
78+
type RenderDocsTopBarProps = Partial<{
79+
widgetVersion: string;
80+
markdownUrl: string;
81+
githubUrl: string;
82+
latestRelease: {
83+
version: string;
84+
description: string;
85+
tinyExcerpt: string;
86+
date: string;
87+
} | null;
88+
changelogContent: React.ReactNode;
89+
previous: {
90+
url: string;
91+
name: string;
92+
} | null;
93+
next: {
94+
url: string;
95+
name: string;
96+
} | null;
97+
}>;
98+
99+
async function renderDocsTopBar(props: RenderDocsTopBarProps = {}) {
100+
const { DocsTopBarClient } = await modulePromise;
101+
102+
return renderToStaticMarkup(
103+
<DocsTopBarClient
104+
githubUrl="https://github.com/cossistantcom/cossistant/blob/main/apps/web/content/docs/quickstart/index.mdx"
105+
latestRelease={{
106+
version: "0.1.2",
107+
description: "Script embeds and AI clarification",
108+
tinyExcerpt: "Script embeds and AI clarification",
109+
date: "2026-04-20",
110+
}}
111+
markdownUrl="/docs/quickstart.mdx"
112+
next={{ name: "React", url: "/docs/quickstart/react" }}
113+
previous={{ name: "Overview", url: "/docs" }}
114+
widgetVersion="0.2.0"
115+
{...props}
116+
/>
117+
);
118+
}
119+
120+
describe("DocsTopBarClient", () => {
121+
it("renders the widget version, what's new link, and page actions", async () => {
122+
const html = await renderDocsTopBar();
123+
124+
expect(html).toContain("Widget v0.2.0");
125+
expect(html).toContain('data-slot="docs-topbar-whats-new"');
126+
expect(html).toContain("NEW: Script embeds and AI clarification");
127+
expect(html).toContain('href="/changelog"');
128+
expect(html).toContain('href="/docs"');
129+
expect(html).toContain('href="/docs/quickstart/react"');
130+
expect(html).toContain('data-icon="arrow-left"');
131+
expect(html).toContain('data-icon="arrow-right"');
132+
expect(html).toContain('data-slot="mock-copy-page"');
133+
expect(html).toContain('data-slot="mock-view-options"');
134+
});
135+
136+
it("omits missing neighbour controls while keeping copy actions", async () => {
137+
const html = await renderDocsTopBar({
138+
next: null,
139+
previous: null,
140+
});
141+
142+
expect(html).not.toContain('data-icon="arrow-left"');
143+
expect(html).not.toContain('data-icon="arrow-right"');
144+
expect(html).toContain('data-slot="mock-copy-page"');
145+
expect(html).toContain('data-slot="mock-view-options"');
146+
});
147+
148+
it("keeps the version badge when there is no changelog entry", async () => {
149+
const html = await renderDocsTopBar({
150+
latestRelease: null,
151+
});
152+
153+
expect(html).toContain("Widget v0.2.0");
154+
expect(html).not.toContain('data-slot="docs-topbar-whats-new"');
155+
});
156+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { findNeighbour } from "fumadocs-core/page-tree";
2+
import { GITHUB_URL } from "@/constants";
3+
import { getDocsWidgetRelease } from "@/lib/docs-widget-release";
4+
import { source } from "@/lib/source";
5+
import { DocsTopBarClient } from "./docs-topbar.client";
6+
7+
type DocsTopBarProps = {
8+
currentPageUrl: string;
9+
currentPagePath: string;
10+
};
11+
12+
export async function DocsTopBar({
13+
currentPageUrl,
14+
currentPagePath,
15+
}: DocsTopBarProps) {
16+
const [neighbours, release] = await Promise.all([
17+
findNeighbour(source.pageTree, currentPageUrl),
18+
getDocsWidgetRelease(),
19+
]);
20+
21+
return (
22+
<DocsTopBarClient
23+
githubUrl={`${GITHUB_URL}/blob/main/apps/web/content/docs/${currentPagePath}`}
24+
latestRelease={release.latestRelease}
25+
markdownUrl={`${currentPageUrl}.mdx`}
26+
next={neighbours.next}
27+
previous={neighbours.previous}
28+
widgetVersion={release.widgetVersion}
29+
/>
30+
);
31+
}

0 commit comments

Comments
 (0)