Skip to content

Commit

Permalink
feat: add toc and sidebar in documentation (#154)
Browse files Browse the repository at this point in the history
feat: add TOC and SideBar
  • Loading branch information
r4ai authored Aug 1, 2024
1 parent e5b54ab commit 25cdecc
Show file tree
Hide file tree
Showing 20 changed files with 524 additions and 93 deletions.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions packages/website/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tailwindCSS.experimental.classRegex": ["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
}
4 changes: 3 additions & 1 deletion packages/website/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export default defineConfig({
noExternal: ["@r4ai/remark-callout"],
},
},
redirects: {},
redirects: {
"/docs/en": `${metadata.base}/docs/en/getting-started`,
},
integrations: [
tailwind({
applyBaseStyles: false,
Expand Down
6 changes: 4 additions & 2 deletions packages/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
"@astrojs/tailwind": "^5.1.0",
"@monaco-editor/react": "^4.6.0",
"@prettier/sync": "^0.5.1",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@types/react": "^18.2.66",
Expand Down Expand Up @@ -54,6 +55,7 @@
"rehype-katex": "^7.0.0",
"remark-math": "^6.0.0",
"shiki": "^1.2.0",
"tailwind-scrollbar": "^3.1.0"
"tailwind-scrollbar": "^3.1.0",
"tailwind-variants": "^0.2.1"
}
}
1 change: 1 addition & 0 deletions packages/website/prettier.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
printWidth: 120,
semi: false,
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
tailwindFunctions: ["tv"],
overrides: [
{
files: "*.astro",
Expand Down
93 changes: 30 additions & 63 deletions packages/website/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import meta from "@/lib/metadata"
import { cn } from "@/lib/utils"
import { GitHubLogoIcon, HamburgerMenuIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { type FC, type ReactNode, useEffect, useState } from "react"
import { Nodes } from "./NavSideBar"
import {
Drawer,
DrawerContent,
Expand All @@ -14,70 +15,41 @@ import {
} from "./ui/drawer"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"

type Route = {
label: string
href: string
}

export const routes = {
index: {
label: "Home",
href: `${meta.base}/`,
},
docs: {
label: "Docs",
href: `${meta.base}/docs/en`,
},
playground: {
label: "Playground",
href: `${meta.base}/playground`,
},
} as const satisfies Record<string, Route>

type HeaderProps = {
className?: string
route: (typeof routes)[keyof typeof routes]["href"]
activeSlug: string
}

export const Header: FC<HeaderProps> = ({ route }) => {
export const Header: FC<HeaderProps> = ({ activeSlug }) => {
return (
<TooltipProvider>
<header className="sticky top-0 z-40 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 max-w-screen-2xl flex-row items-center justify-between">
<nav className="hidden sm:block">
<ul className="flex flex-row items-center gap-4">
<nav className="flex flex-row gap-4">
<div className="md:hidden">
<NavDrawer activeSlug={activeSlug} />
</div>
<ul className="hidden flex-row items-center gap-4 sm:flex">
<li>
<Button variant="ghost" className="font-bold" asChild>
<a href={routes.index.href}>{meta.name}</a>
<a href={meta.base}>{meta.name}</a>
</Button>
</li>
<li>
<a
href={routes.docs.href}
className={cn(
"text-muted-foreground transition hover:text-foreground",
route === "/remark-callout/docs/en" && "font-medium text-foreground",
)}
>
{routes.docs.label}
</a>
</li>
<li>
<a
href={routes.playground.href}
className={cn(
"text-muted-foreground transition hover:text-foreground",
route === "/remark-callout/playground" && "font-medium text-foreground",
)}
>
{routes.playground.label}
</a>
</li>
{meta.entries.slice(1).map((entry) => (
<li key={entry.slug}>
<a
href={`${meta.base}${entry.slug}`}
className={cn(
"text-muted-foreground transition hover:text-foreground",
activeSlug.startsWith(entry.slug) && "font-medium text-foreground",
)}
>
{entry.title}
</a>
</li>
))}
</ul>
</nav>
<div className="sm:hidden">
<NavDrawer />
</div>
<div className="flex flex-row">
<IconButton tooltip="GitHub">
<a href={meta.repository.url.href}>
Expand All @@ -92,7 +64,11 @@ export const Header: FC<HeaderProps> = ({ route }) => {
)
}

const NavDrawer: FC = () => {
type NavDrawerProps = {
activeSlug: string
}

const NavDrawer: FC<NavDrawerProps> = ({ activeSlug }) => {
return (
<Drawer>
<DrawerTrigger asChild>
Expand All @@ -103,20 +79,11 @@ const NavDrawer: FC = () => {
<DrawerContent>
<div className="flex flex-col gap-4">
<DrawerHeader>
<DrawerTitle>{meta.name}</DrawerTitle>
<DrawerDescription>{meta.description}</DrawerDescription>
<DrawerTitle className="text-center">{meta.name}</DrawerTitle>
<DrawerDescription className="text-center">{meta.description}</DrawerDescription>
</DrawerHeader>
<nav>
<ul className="mx-auto flex max-w-sm flex-col px-8">
{Object.values(routes).map((route, i) => (
<li key={route.href}>
{i > 0 && <div className="h-[1px] w-full bg-border" />}
<a href={route.href} className="block py-2.5 text-left hover:underline">
{route.label}
</a>
</li>
))}
</ul>
<Nodes className="mx-auto max-w-sm px-8" nodes={meta.entries} activeSlug={activeSlug} nested={false} />
</nav>
<DrawerFooter>
<div className="ml-auto flex flex-row">
Expand Down
122 changes: 122 additions & 0 deletions packages/website/src/components/NavSideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import metadata from "@/lib/metadata"
import { cn } from "@/lib/utils"
import type { ComponentPropsWithoutRef, FC } from "react"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"

export type FileNode = {
type: "file"
slug: string
title: string
}

export type DirectoryNode = {
type: "directory"
slug: string
title: string
children: Node[]
}

export type Node = FileNode | DirectoryNode

export type NavSideBarProps = ComponentPropsWithoutRef<"div"> & {
entries: Node[]
activeSlug: string
}

export const NavSideBar: FC<NavSideBarProps> = ({ className, entries, activeSlug, ...props }) => {
return (
<div>
<div
className={cn("sticky top-[calc(3.5rem+1px)] hidden min-w-56 flex-col gap-3 p-4 md:flex", className)}
{...props}
>
<span className="inline-block w-full border-b pb-2 font-bold">Documentation</span>
<nav>
<Nodes nodes={entries} activeSlug={activeSlug} nested={false} />
</nav>
</div>
</div>
)
}

export type NodesProps = ComponentPropsWithoutRef<"div"> & {
nodes: Node[]
activeSlug: string
nested: boolean
}

export const Nodes: FC<NodesProps> = ({ nodes, activeSlug, nested, ...props }) => {
return (
<div {...props}>
{nodes.map((node) =>
node.type === "directory" ? (
<Directory key={node.slug} node={node} activeSlug={activeSlug} nested={nested} />
) : (
<File key={node.slug} node={node} activeSlug={activeSlug} nested={nested} />
),
)}
</div>
)
}

type FileProps = ComponentPropsWithoutRef<"a"> & {
node: Node & { type: "file" }
activeSlug: string
nested: boolean
}

const File: FC<FileProps> = ({ className, node, activeSlug, nested, ...props }) => {
return (
<a
href={`${metadata.base}${node.slug}`}
data-active={node.slug === activeSlug}
className={cn(
"inline-block w-full py-1.5 text-muted-foreground transition data-[active=true]:font-bold data-[active=true]:text-foreground hover:text-foreground",
nested && "ml-1 border-l pl-4",
className,
)}
{...props}
>
{node.title}
</a>
)
}

type DirectoryProps = {
node: Node & { type: "directory" }
activeSlug: string
nested: boolean
}

const Directory: FC<DirectoryProps> = ({ node, activeSlug, nested }) => {
console.log(node)
return (
<Accordion
type="single"
collapsible
defaultValue={has(node.children, activeSlug) ? node.title : undefined}
className={cn(nested && "ml-1 border-l pl-4")}
>
<AccordionItem className="border-b-0" value={node.title}>
<AccordionTrigger className="py-1.5 text-muted-foreground hover:text-foreground hover:no-underline">
{node.title}
</AccordionTrigger>
<AccordionContent className="pb-1.5 text-base">
<Nodes nodes={node.children} activeSlug={activeSlug} nested />
</AccordionContent>
</AccordionItem>
</Accordion>
)
}

const has = (nodes: Node[], activeSlug: string) => {
for (const node of nodes) {
if (node.type === "file" && node.slug === activeSlug) {
return true
}
if (node.type === "directory" && has(node.children, activeSlug)) {
return true
}
}
return false
}
79 changes: 79 additions & 0 deletions packages/website/src/components/TOC.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { cn } from "@/lib/utils"
import type { MarkdownHeading } from "astro"
import { type ComponentPropsWithoutRef, type FC, useEffect } from "react"
import { tv } from "tailwind-variants"

export type TOCProps = ComponentPropsWithoutRef<"div"> & {
headings: MarkdownHeading[]
}

export const TOC: FC<TOCProps> = ({ headings, className, ...props }) => {
useEffect(() => {
let activeTocItem: Element | null = null
const observer = new IntersectionObserver((entries) => {
const entry = entries.reduce((prev, current) =>
current.intersectionRatio > prev.intersectionRatio ? current : prev,
)
if (entry.intersectionRatio > 0) {
const toActivateTocItem = document.querySelector(`a[href="#${entry.target.id}"]`)
activeTocItem?.setAttribute("data-active", "false")
toActivateTocItem?.setAttribute("data-active", "true")
activeTocItem = toActivateTocItem
}
})
for (const headingElm of document.querySelectorAll("h2[id], h3[id], h4[id], h5[id]")) {
observer.observe(headingElm)
}
}, [])

return (
<div>
<div
className={cn("sticky top-[calc(3.5rem+1px)] mx-8 hidden min-w-56 flex-col gap-4 xl:flex", className)}
{...props}
>
<span className="mt-6 font-bold">On this page</span>
<ul className="flex flex-col gap-1">
{headings.map((heading) => (
<TOCItem key={heading.slug} heading={heading} />
))}
</ul>
</div>
</div>
)
}

type TOCItemProps = ComponentPropsWithoutRef<"a"> & {
heading: MarkdownHeading
}

const TOCItem: FC<TOCItemProps> = ({ heading, className, ...props }) => {
return (
<li>
<a
data-active="false"
className={tocItem({
depth: Math.max(1, Math.min(heading.depth, 5)) as 1 | 2 | 3 | 4 | 5,
className,
})}
href={`#${heading.slug}`}
{...props}
>
{heading.text}
</a>
</li>
)
}

const tocItem = tv({
base: "text-muted-foreground data-[active=true]:text-foreground",
variants: {
depth: {
1: "ml-0",
2: "ml-0",
3: "ml-4",
4: "ml-8",
5: "ml-12",
},
},
})
Loading

0 comments on commit 25cdecc

Please sign in to comment.