Skip to content

Commit f7935cf

Browse files
committed
feat: fix landing + doc navigation on mobike
1 parent ed44d1a commit f7935cf

23 files changed

Lines changed: 1312 additions & 217 deletions
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, it, mock } from "bun:test";
2+
import React from "react";
3+
import { renderToStaticMarkup } from "react-dom/server";
4+
import { SidebarProvider } from "@/components/ui/sidebar";
5+
6+
mock.module("next/link", () => ({
7+
default: ({
8+
children,
9+
href,
10+
...props
11+
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & {
12+
children: React.ReactNode;
13+
href: string;
14+
}) => (
15+
<a href={href} {...props}>
16+
{children}
17+
</a>
18+
),
19+
}));
20+
21+
const modulePromise = import("./docs-nav-tree");
22+
23+
const tree = {
24+
children: [
25+
{
26+
$id: "getting-started",
27+
children: [
28+
{
29+
name: "Quickstart",
30+
type: "page" as const,
31+
url: "/docs/quickstart",
32+
},
33+
{
34+
name: "Install",
35+
type: "page" as const,
36+
url: "/docs/install",
37+
},
38+
],
39+
name: "Getting Started",
40+
type: "folder" as const,
41+
},
42+
],
43+
};
44+
45+
async function renderDocsNavTree(pathname: string) {
46+
const { DocsNavTree } = await modulePromise;
47+
48+
return renderToStaticMarkup(
49+
<React.StrictMode>
50+
<SidebarProvider>
51+
<DocsNavTree pathname={pathname} tree={tree} />
52+
</SidebarProvider>
53+
</React.StrictMode>
54+
);
55+
}
56+
57+
describe("DocsNavTree", () => {
58+
it("renders section labels and page links", async () => {
59+
const html = await renderDocsNavTree("/docs/quickstart");
60+
61+
expect(html).toContain("Getting Started");
62+
expect(html).toContain('href="/docs/quickstart"');
63+
expect(html).toContain('href="/docs/install"');
64+
});
65+
66+
it("marks the current docs page as active", async () => {
67+
const html = await renderDocsNavTree("/docs/install");
68+
69+
expect(html).toContain('data-docs-url="/docs/install"');
70+
expect(html).toContain('aria-current="page"');
71+
expect(html).toContain('data-active="true"');
72+
});
73+
});
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import {
5+
SidebarGroup,
6+
SidebarGroupContent,
7+
SidebarGroupLabel,
8+
SidebarMenu,
9+
SidebarMenuButton,
10+
SidebarMenuItem,
11+
} from "@/components/ui/sidebar";
12+
13+
type DocsPageNode = {
14+
name?: React.ReactNode;
15+
type: "page";
16+
url: string;
17+
};
18+
19+
type DocsTreeNode = {
20+
$id?: string;
21+
children?: DocsTreeNode[];
22+
name?: React.ReactNode;
23+
type: string;
24+
url?: string;
25+
};
26+
27+
type DocsPageTree = {
28+
children: DocsTreeNode[];
29+
};
30+
31+
type DocsNavTreeProps = {
32+
pathname: string;
33+
tree: DocsPageTree;
34+
wrapLink?: (props: {
35+
element: React.ReactElement;
36+
href: string;
37+
isActive: boolean;
38+
name?: React.ReactNode;
39+
}) => React.ReactElement;
40+
};
41+
42+
function isFolderNode(node: DocsTreeNode): node is DocsTreeNode & {
43+
children: DocsTreeNode[];
44+
type: "folder";
45+
} {
46+
return node.type === "folder" && Array.isArray(node.children);
47+
}
48+
49+
function isPageNode(node: DocsTreeNode): node is DocsPageNode {
50+
return node.type === "page" && typeof node.url === "string";
51+
}
52+
53+
export function DocsNavTree({ pathname, tree, wrapLink }: DocsNavTreeProps) {
54+
return (
55+
<div data-slot="docs-nav-tree">
56+
{tree.children.map((item, index) => (
57+
<SidebarGroup key={item.$id ?? `${item.type}-${index}`}>
58+
<SidebarGroupLabel className="font-medium text-muted-foreground">
59+
{item.name}
60+
</SidebarGroupLabel>
61+
<SidebarGroupContent>
62+
{isFolderNode(item) ? (
63+
<SidebarMenu className="gap-0.5">
64+
{item.children.map((subItem) => {
65+
if (!isPageNode(subItem)) {
66+
return null;
67+
}
68+
69+
const isActive = subItem.url === pathname;
70+
const button = (
71+
<SidebarMenuButton
72+
aria-current={isActive ? "page" : undefined}
73+
asChild
74+
className="after:-inset-y-1 relative h-[30px] 3xl:fixed:w-full w-full 3xl:fixed:max-w-48 overflow-visible border border-transparent font-medium text-[0.8rem] after:absolute after:inset-x-0 after:z-0 after:rounded data-[active=true]:border-transparent data-[active=true]:bg-background-300"
75+
data-docs-url={subItem.url}
76+
data-slot="docs-nav-link"
77+
isActive={isActive}
78+
>
79+
<Link href={subItem.url}>{subItem.name}</Link>
80+
</SidebarMenuButton>
81+
);
82+
83+
return (
84+
<SidebarMenuItem key={subItem.url}>
85+
{wrapLink
86+
? wrapLink({
87+
element: button,
88+
href: subItem.url,
89+
isActive,
90+
name: subItem.name,
91+
})
92+
: button}
93+
</SidebarMenuItem>
94+
);
95+
})}
96+
</SidebarMenu>
97+
) : null}
98+
</SidebarGroupContent>
99+
</SidebarGroup>
100+
))}
101+
</div>
102+
);
103+
}
104+
105+
export type { DocsPageTree };

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

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
11
"use client";
22

3-
import Link from "next/link";
43
import { usePathname } from "next/navigation";
5-
import {
6-
Sidebar,
7-
SidebarContent,
8-
SidebarGroup,
9-
SidebarGroupContent,
10-
SidebarGroupLabel,
11-
SidebarMenu,
12-
SidebarMenuButton,
13-
SidebarMenuItem,
14-
} from "@/components/ui/sidebar";
15-
16-
import type { source } from "@/lib/source";
4+
import { Sidebar, SidebarContent } from "@/components/ui/sidebar";
5+
import { DocsNavTree, type DocsPageTree } from "./docs-nav-tree";
176

187
export function DocsSidebar({
198
tree,
209
...props
21-
}: React.ComponentProps<typeof Sidebar> & { tree: typeof source.pageTree }) {
10+
}: React.ComponentProps<typeof Sidebar> & { tree: DocsPageTree }) {
2211
const pathname = usePathname();
2312

2413
return (
@@ -29,33 +18,7 @@ export function DocsSidebar({
2918
>
3019
<SidebarContent className="no-scrollbar px-0 pb-12">
3120
<div className="h-(--top-spacing) shrink-0" />
32-
{tree.children.map((item: (typeof tree.children)[number]) => (
33-
<SidebarGroup key={item.$id}>
34-
<SidebarGroupLabel className="font-medium text-muted-foreground">
35-
{item.name}
36-
</SidebarGroupLabel>
37-
<SidebarGroupContent>
38-
{item.type === "folder" && (
39-
<SidebarMenu className="gap-0.5">
40-
{item.children.map(
41-
(subItem: (typeof item.children)[number]) =>
42-
subItem.type === "page" && (
43-
<SidebarMenuItem key={subItem.url}>
44-
<SidebarMenuButton
45-
asChild
46-
className="after:-inset-y-1 relative h-[30px] 3xl:fixed:w-full w-full 3xl:fixed:max-w-48 overflow-visible border border-transparent font-medium text-[0.8rem] after:absolute after:inset-x-0 after:z-0 after:rounded data-[active=true]:border-transparent data-[active=true]:bg-background-300"
47-
isActive={subItem.url === pathname}
48-
>
49-
<Link href={subItem.url}>{subItem.name}</Link>
50-
</SidebarMenuButton>
51-
</SidebarMenuItem>
52-
)
53-
)}
54-
</SidebarMenu>
55-
)}
56-
</SidebarGroupContent>
57-
</SidebarGroup>
58-
))}
21+
<DocsNavTree pathname={pathname} tree={tree} />
5922
</SidebarContent>
6023
</Sidebar>
6124
);

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
AccordionTrigger,
2525
} from "@/components/ui/accordion";
2626
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
27+
import { Background } from "@/components/ui/background";
2728
import { Button } from "@/components/ui/button";
2829
import Icon from "@/components/ui/icons";
2930
import { getIconForLanguageExtension } from "@/components/ui/logos";
@@ -355,14 +356,23 @@ export const mdxComponents = {
355356
{...props}
356357
/>
357358
),
358-
LinkedCard: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
359+
LinkedCard: ({
360+
className,
361+
children,
362+
...props
363+
}: React.ComponentProps<typeof Link>) => (
359364
<Link
360365
className={cn(
361-
"flex w-full flex-col items-start gap-2 rounded-[1px] border bg-background-100 p-4 text-surface-foreground transition-colors hover:bg-background-200 sm:p-6",
366+
"group relative w-full rounded-[1px] border bg-background-100 p-4 text-surface-foreground transition-colors hover:bg-background-200 sm:p-6",
362367
className
363368
)}
364369
{...props}
365-
/>
370+
>
371+
<Background className="z-0 opacity-0 transition-all duration-300 group-hover:opacity-100" />
372+
<div className="pointer-events-none relative inset-0 z-10 flex flex-col items-start gap-2 from-background-200 via-background-200 to-transparent transition-colors group-hover:bg-linear-to-t">
373+
{children}
374+
</div>
375+
</Link>
366376
),
367377
CodeBlockWrapper: ({ ...props }) => (
368378
<CodeBlockWrapper className="rounded-md border" {...props} />

apps/web/src/app/(lander-docs)/components/precision-flow-playback.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,15 +501,35 @@ function PrecisionFlowConversationStage({
501501
);
502502
}
503503

504-
function PrecisionFlowRightPanel({ children }: { children: React.ReactNode }) {
504+
function PrecisionFlowRightPanel({
505+
children,
506+
showOpeningMessageRadial = false,
507+
}: {
508+
children: React.ReactNode;
509+
showOpeningMessageRadial?: boolean;
510+
}) {
505511
return (
506512
<div
507513
className="relative flex h-full w-full flex-1 overflow-hidden bg-background dark:bg-background-50"
508514
data-precision-background-trail="enabled"
509515
>
510516
<Background fieldOpacity={0.06} interactive={true} pointerTrail={true} />
511-
<div className="pointer-events-none absolute inset-y-0 left-0 z-0 w-full bg-linear-to-r from-background to-transparent" />
512-
<div className="pointer-events-none absolute inset-y-0 left-0 z-0 w-full bg-linear-to-r from-background to-transparent" />
517+
<div className="pointer-events-none absolute inset-y-0 left-0 z-[1] w-full bg-linear-to-r from-background to-transparent" />
518+
<div className="pointer-events-none absolute inset-y-0 left-0 z-[1] w-full bg-linear-to-r from-background to-transparent" />
519+
{showOpeningMessageRadial ? (
520+
<div
521+
className="pointer-events-none absolute inset-0 z-[2] flex justify-center px-4 pb-16 lg:px-8 lg:py-16 xl:px-1"
522+
data-precision-opening-radial="true"
523+
>
524+
<div
525+
className={cn(
526+
"my-auto h-full w-full max-w-2xl",
527+
"bg-[radial-gradient(ellipse_68%_52%_at_50%_44%,var(--background)_0%,var(--background)_30%,transparent_74%)]",
528+
"dark:bg-[radial-gradient(ellipse_68%_52%_at_50%_44%,var(--background-50)_0%,var(--background-50)_30%,transparent_74%)]"
529+
)}
530+
/>
531+
</div>
532+
) : null}
513533
<div className="pointer-events-none relative z-10 flex h-full w-full flex-1 px-4 pb-16 lg:px-8 lg:py-16 xl:px-1">
514534
{children}
515535
</div>
@@ -863,9 +883,16 @@ export function PrecisionFlowPlaybackStage() {
863883
phase === "gap_search_result" ||
864884
phase === "human_handoff" ||
865885
phase === "clarify_transition";
886+
const showOpeningMessageRadial =
887+
phase === "visitor_question" ||
888+
phase === "gap_search_loading" ||
889+
phase === "gap_search_result" ||
890+
phase === "human_handoff";
866891

867892
return (
868-
<PrecisionFlowRightPanel>
893+
<PrecisionFlowRightPanel
894+
showOpeningMessageRadial={showOpeningMessageRadial}
895+
>
869896
{phase === "faq_created" ? (
870897
<PrecisionFlowFaqCreatedCard draft={scene.faqDraft} />
871898
) : (

apps/web/src/app/(lander-docs)/components/precision-flow-section.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe("PrecisionFlowSection", () => {
4545
expect(html).toContain('data-fake-conversation-layout-mode="centered"');
4646
expect(html).toContain('data-precision-stage-layout="centered"');
4747
expect(html).toContain('data-precision-background-trail="enabled"');
48+
expect(html).toContain('data-precision-opening-radial="true"');
4849
expect(html).toContain('data-background="aurora-ascii"');
4950
expect(html).not.toContain('data-composer-frame="default"');
5051
expect(html).not.toContain('data-composer-layout-mode="inline"');
@@ -83,6 +84,7 @@ describe("PrecisionFlowSection", () => {
8384
expect(html).toContain(
8485
"No saved answer for &quot;delete account&quot; yet"
8586
);
87+
expect(html).toContain('data-precision-opening-radial="true"');
8688
expect(html).not.toContain("Type your message...");
8789
expect(html).not.toContain('type="file"');
8890
});
@@ -107,6 +109,7 @@ describe("PrecisionFlowSection", () => {
107109
expect(html).toContain("Reply");
108110
expect(html).toContain("Private note");
109111
expect(html).toContain('type="file"');
112+
expect(html).not.toContain('data-precision-opening-radial="true"');
110113
expect(html).not.toContain("autofocus");
111114
expect(html).not.toContain("How do I delete my account?");
112115
expect(html).not.toContain(
@@ -161,6 +164,7 @@ describe("PrecisionFlowSection", () => {
161164
"I don&#x27;t know this one yet, so I&#x27;m asking the team and saving the answer for next time."
162165
);
163166
expect(html).toContain("Clarification");
167+
expect(html).not.toContain('data-precision-opening-radial="true"');
164168
});
165169

166170
it("shows the first-step next cursor before advancing to the follow-up question", () => {
@@ -258,6 +262,7 @@ describe("PrecisionFlowSection", () => {
258262
expect(html).toContain("w-[84%]");
259263
expect(html).toContain("w-[86%]");
260264
expect(html).toContain("self-center");
265+
expect(html).not.toContain('data-precision-opening-radial="true"');
261266
expect(html).not.toContain('data-clarification-slot="draft-ready-banner"');
262267
expect(html).not.toContain("Knowledge base updated");
263268
});

0 commit comments

Comments
 (0)