Skip to content

Commit 1b7623d

Browse files
committed
feat(app): share one sidebar between the docs and the playground
1 parent 6e3c2c6 commit 1b7623d

20 files changed

Lines changed: 499 additions & 445 deletions

‎app/(chat)/layout.tsx‎

Lines changed: 0 additions & 18 deletions
This file was deleted.

‎app/(docs)/[[...slug]]/page.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ export default async function DocsPage(props: PageProps) {
2424

2525
return (
2626
<div className="flex min-h-full flex-col">
27-
<header className="sticky top-0 z-10 isolate flex h-13 shrink-0 items-center justify-between gap-3 pr-4 pl-3 md:pl-6">
27+
<header className="sticky top-0 z-10 isolate flex h-13 shrink-0 items-center justify-between gap-3 px-3">
2828
{/* Content dissolves under the bar instead of cutting off: a progressive
29-
blur plus a fade that reach past the bar's bottom edge. */}
29+
blur plus a fade. */}
3030
<ProgressiveBlur
3131
direction="top"
3232
blurIntensity={0.5}
33-
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-20 bg-linear-to-b from-40% from-secondary-bg to-transparent"
33+
className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-14 bg-linear-to-b from-secondary-bg to-transparent"
3434
/>
3535
<div className="flex min-w-0 items-center gap-2">
3636
<DocsTopBarTrigger />

‎app/(docs)/layout.tsx‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { ReactNode } from "react";
2-
import { DocsShell } from "@/components/docs/docs-shell";
3-
import { source } from "@/lib/docs/source";
42

3+
// The site shell lives in the root layout; the docs only add their own scroller.
4+
// The panel scrolls, not the window, so the frame stays put around it.
55
export default function DocsLayout({ children }: { children: ReactNode }) {
6-
return <DocsShell tree={source.pageTree}>{children}</DocsShell>;
6+
return (
7+
<div data-docs-scroll="" className="h-full w-full overflow-y-auto">
8+
{children}
9+
</div>
10+
);
711
}

‎app/globals.css‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,22 @@
588588
calc(100% - 32px) 100%;
589589
}
590590

591+
/* Switching between the docs and the playground cross-dissolves the viewport (a
592+
ViewTransition in SiteShell). The root swaps live rather than fading a frozen
593+
snapshot, so the sidebar's own slide plays through. */
594+
/* biome-ignore lint/correctness/noUnknownTypeSelector: `root` is a view-transition name, not an element. */
595+
::view-transition-old(root) {
596+
display: none;
597+
}
598+
/* biome-ignore lint/correctness/noUnknownTypeSelector: `root` is a view-transition name, not an element. */
599+
::view-transition-new(root) {
600+
animation: none;
601+
}
602+
::view-transition-group(site-viewport) {
603+
animation-duration: 220ms;
604+
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
605+
}
606+
591607
/* Docs code blocks: a line-number gutter on multi-line blocks. */
592608
[data-code-block] pre[data-line-numbers] code {
593609
counter-reset: line;

‎app/layout.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
22
import { Inter } from "next/font/google";
33
import localFont from "next/font/local";
44
import { Providers } from "@/components/providers";
5+
import { SiteShell } from "@/components/site-shell";
6+
import { source } from "@/lib/docs/source";
57
import "streamdown/styles.css";
68
import "./globals.css";
79
const inter = Inter({
@@ -43,7 +45,9 @@ export default function RootLayout({
4345
className={`${inter.variable} ${ioskeleyMono.variable} antialiased`}
4446
suppressHydrationWarning
4547
>
46-
<Providers>{children}</Providers>
48+
<Providers>
49+
<SiteShell tree={source.pageTree}>{children}</SiteShell>
50+
</Providers>
4751
</body>
4852
</html>
4953
);
Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
"use client";
2-
import { Bin, BookOpen, MoreVertical, SquarePen } from "@keyline-icons/react";
3-
// Keyline has no brand icons, so these two stay on Tabler.
4-
import { IconBrandGithub, IconBrandNpm } from "@tabler/icons-react";
2+
import { Bin, MoreVertical, SquarePen } from "@keyline-icons/react";
53
import Link from "next/link";
64
import { usePathname, useRouter } from "next/navigation";
7-
import { LogoTile } from "@/components/icons/logo-tile";
8-
import { IconButton } from "@/components/ui/icon-button";
95
import { Sidebar } from "@/components/ui/sidebar";
106
import { deleteChatInstance } from "@/lib/chat-instance";
117
import { useChatStore } from "@/lib/store/chat";
128
import DropdownMenu from "./ui/dropdown-menu";
139

14-
export const AppSidebar = () => {
10+
/** The playground half of the site sidebar: new chat and the threads. */
11+
export const ChatNav = () => {
1512
const pathname = usePathname();
1613
const router = useRouter();
1714
const chats = useChatStore((state) => state.chats);
@@ -27,17 +24,10 @@ export const AppSidebar = () => {
2724
};
2825

2926
return (
30-
<Sidebar>
31-
<Sidebar.Header className="h-10 shrink-0 flex-row items-center justify-between pr-1 pl-2">
32-
<Link href="/playground" className="flex items-center gap-2.5 text-ink-primary">
33-
<LogoTile />
34-
<span className="font-semibold text-md tracking-[-0.01em]">intentface/chat</span>
35-
</Link>
36-
<Sidebar.Trigger className="size-7" />
37-
</Sidebar.Header>
27+
<>
3828
<Link
3929
href="/playground"
40-
className="flex h-[34px] shrink-0 items-center gap-2 rounded-md bg-raised px-2.5 font-medium text-ink-primary text-sm shadow-raised transition-[background-color] hover:bg-raised-hover focus-visible:outline-2 focus-visible:outline-accent-bg/60 focus-visible:outline-offset-2"
30+
className="flex h-[30px] shrink-0 items-center gap-2 rounded-md bg-raised px-2.5 font-medium text-ink-primary text-sm shadow-raised transition-[background-color] hover:bg-raised-hover focus-visible:outline-2 focus-visible:outline-accent-bg/60 focus-visible:outline-offset-2"
4131
>
4232
<SquarePen className="size-[15px] text-ink-body" />
4333
New chat
@@ -83,49 +73,6 @@ export const AppSidebar = () => {
8373
</Sidebar.GroupContent>
8474
</Sidebar.Group>
8575
</Sidebar.Content>
86-
<Sidebar.Footer className="flex-row items-center justify-between border-ink-primary/8 border-t pt-2 pr-1 pl-0.5">
87-
<Link
88-
href="/"
89-
className="flex h-[30px] items-center gap-[7px] rounded-md pr-2 pl-1.5 font-medium text-ink-body text-sm transition-colors hover:bg-ink-primary/5 hover:text-ink-primary focus-visible:outline-2 focus-visible:outline-accent-bg/60"
90-
>
91-
<BookOpen className="size-[15px] text-ink-secondary" />
92-
Docs
93-
</Link>
94-
<div className="flex items-center gap-0.5">
95-
<IconButton
96-
variant="ghost"
97-
size="sm"
98-
nativeButton={false}
99-
aria-label="GitHub"
100-
className="rounded-md"
101-
render={
102-
<a
103-
href="https://github.com/Intentface/intentface-chat"
104-
target="_blank"
105-
rel="noreferrer"
106-
>
107-
<IconBrandGithub />
108-
</a>
109-
}
110-
/>
111-
<IconButton
112-
variant="ghost"
113-
size="sm"
114-
nativeButton={false}
115-
aria-label="npm"
116-
className="rounded-md"
117-
render={
118-
<a
119-
href="https://www.npmjs.com/package/@intentface/chat"
120-
target="_blank"
121-
rel="noreferrer"
122-
>
123-
<IconBrandNpm />
124-
</a>
125-
}
126-
/>
127-
</div>
128-
</Sidebar.Footer>
129-
</Sidebar>
76+
</>
13077
);
13178
};

‎components/chat-shell.tsx‎

Lines changed: 0 additions & 31 deletions
This file was deleted.

‎components/docs/docs-nav.tsx‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { usePathname } from "next/navigation";
5+
import type { ReactNode } from "react";
6+
import { Sidebar } from "@/components/ui/sidebar";
7+
import type { SearchSuggestion } from "./docs-search";
8+
9+
// Structural mirror of fumadocs' page-tree nodes (typeof source.pageTree). We
10+
// only render the node shapes our docs use: pages, separators and one level of folder.
11+
type TreeItem = { type: "page"; name: ReactNode; url: string };
12+
type TreeFolder = { type: "folder"; name: ReactNode; children: TreeNode[] };
13+
type TreeSeparator = { type: "separator"; name?: ReactNode };
14+
type TreeNode = TreeItem | TreeFolder | TreeSeparator;
15+
16+
export type NavGroup = { label?: ReactNode; items: TreeItem[] };
17+
18+
export type DocsTree = { children: TreeNode[] };
19+
20+
// Pages to flag as new in the sidebar. Drop a URL once its primitive has been
21+
// out for a release or two.
22+
const NEW_PAGES = new Set(["/primitives/shell", "/primitives/nav", "/primitives/tabs"]);
23+
24+
// A separator opens a labelled group for the root pages after it; a folder is a
25+
// group of its own.
26+
export const toGroups = (nodes: TreeNode[]): NavGroup[] => {
27+
const groups: NavGroup[] = [];
28+
let open: NavGroup | null = null;
29+
30+
for (const node of nodes) {
31+
switch (node.type) {
32+
case "separator":
33+
open = { label: node.name, items: [] };
34+
groups.push(open);
35+
break;
36+
case "folder":
37+
groups.push({
38+
label: node.name,
39+
items: node.children.filter((child): child is TreeItem => child.type === "page"),
40+
});
41+
open = null;
42+
break;
43+
case "page":
44+
if (!open) {
45+
open = { items: [] };
46+
groups.push(open);
47+
}
48+
open.items.push(node);
49+
break;
50+
}
51+
}
52+
53+
return groups;
54+
};
55+
56+
// The search palette opens on these before anything is typed.
57+
export const toSuggestions = (groups: NavGroup[]): SearchSuggestion[] =>
58+
groups
59+
.flatMap((group) => group.items)
60+
.filter((item) => item.url.startsWith("/primitives/"))
61+
.map((item) => ({
62+
url: item.url,
63+
title: typeof item.name === "string" ? item.name : item.url,
64+
}));
65+
66+
const NavLink = ({ item }: { item: TreeItem }) => {
67+
const pathname = usePathname();
68+
69+
return (
70+
<Sidebar.MenuItem>
71+
<Sidebar.MenuButton
72+
isActive={pathname === item.url}
73+
render={
74+
<Link href={item.url}>
75+
<span className="min-w-0 flex-1 truncate">{item.name}</span>
76+
{NEW_PAGES.has(item.url) && (
77+
<span className="flex h-[18px] shrink-0 items-center rounded-[5px] bg-accent-bg/10 px-1.5 font-medium text-2xs text-accent-bg dark:bg-accent-bg/15">
78+
New
79+
</span>
80+
)}
81+
</Link>
82+
}
83+
/>
84+
</Sidebar.MenuItem>
85+
);
86+
};
87+
88+
/** The docs half of the site sidebar: the page tree. */
89+
export const DocsNav = ({ groups }: { groups: NavGroup[] }) => (
90+
<Sidebar.Content className="gap-[18px]">
91+
{groups.map((group, index) => (
92+
<Sidebar.Group key={typeof group.label === "string" ? group.label : index}>
93+
{group.label && <Sidebar.GroupLabel>{group.label}</Sidebar.GroupLabel>}
94+
<Sidebar.Menu>
95+
{group.items.map((item) => (
96+
<NavLink key={item.url} item={item} />
97+
))}
98+
</Sidebar.Menu>
99+
</Sidebar.Group>
100+
))}
101+
</Sidebar.Content>
102+
);

‎components/docs/docs-search.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export const DocsSearch = ({ suggestions = [] }: { suggestions?: SearchSuggestio
253253
variant="ghost"
254254
aria-label="Search docs"
255255
aria-keyshortcuts="Meta+K Control+K"
256-
className="size-7 rounded-md"
256+
className="size-7 rounded-full"
257257
>
258258
<Search />
259259
</IconButton>

‎components/docs/docs-shell.tsx‎

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)