From 34fc6bf5f2bd266226d13936bc5e89a4bce6ed95 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 22 Apr 2026 10:46:21 +0530 Subject: [PATCH 01/27] feat: add prev/next nav fields to Page type Prepare Page type for backend-provided prev/next links used by the docs sub-navbar. Adds PageNavLink/PageNav types, extends Page via extends PageNav, unifies PageData with Page in page context, and threads null placeholders through SSR + hydration until backend computes real values. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/chronicle/src/lib/page-context.tsx | 33 +++++++------------ packages/chronicle/src/pages/DocsPage.tsx | 7 +--- .../chronicle/src/server/entry-client.tsx | 6 +++- .../chronicle/src/server/entry-server.tsx | 4 +++ packages/chronicle/src/types/content.ts | 12 ++++++- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/chronicle/src/lib/page-context.tsx b/packages/chronicle/src/lib/page-context.tsx index 9487a9d..3657e2f 100644 --- a/packages/chronicle/src/lib/page-context.tsx +++ b/packages/chronicle/src/lib/page-context.tsx @@ -10,21 +10,14 @@ import type { ApiSpec } from '@/lib/openapi'; import { resolveRoute, RouteType } from '@/lib/route-resolver'; import type { VersionContext } from '@/lib/version-source'; import { LATEST_CONTEXT } from '@/lib/version-source'; -import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types'; +import type { ChronicleConfig, Frontmatter, Page, Root, TableOfContents } from '@/types'; export type MdxLoader = (relativePath: string) => Promise<{ content: ReactNode; toc: TableOfContents }>; -interface PageData { - slug: string[]; - frontmatter: Frontmatter; - content: ReactNode; - toc: TableOfContents; -} - interface PageContextValue { config: ChronicleConfig; tree: Root; - page: PageData | null; + page: Page | null; errorStatus: number | null; apiSpecs: ApiSpec[]; version: VersionContext; @@ -54,18 +47,18 @@ export function usePageContext(): PageContextValue { interface PageProviderProps { initialConfig: ChronicleConfig; initialTree: Root; - initialPage: PageData | null; + initialPage: Page | null; initialApiSpecs: ApiSpec[]; initialVersion: VersionContext; loadMdx: MdxLoader; children: ReactNode; } -function getInitialErrorStatus( - page: PageData | null, - config: ChronicleConfig, - pathname: string, -): number | null { +function isApisRoute(pathname: string): boolean { + return pathname === '/apis' || pathname.startsWith('/apis/'); +} + +function getInitialErrorStatus(page: Page | null, config: ChronicleConfig, pathname: string): number | null { if (page) return null; const route = resolveRoute(pathname, config); if (route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage) return null; @@ -85,10 +78,8 @@ export function PageProvider({ }: PageProviderProps) { const { pathname } = useLocation(); const [tree] = useState(initialTree); - const [page, setPage] = useState(initialPage); - const [errorStatus, setErrorStatus] = useState( - getInitialErrorStatus(initialPage, initialConfig, pathname), - ); + const [page, setPage] = useState(initialPage); + const [errorStatus, setErrorStatus] = useState(getInitialErrorStatus(initialPage, initialConfig, pathname)); const [apiSpecs, setApiSpecs] = useState(initialApiSpecs); const [version, setVersion] = useState(initialVersion); const [currentPath, setCurrentPath] = useState(pathname); @@ -140,12 +131,12 @@ export function PageProvider({ } return res.json(); }) - .then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string } | undefined) => { + .then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string; prev: Page['prev']; next: Page['next'] } | undefined) => { if (cancelled.current || !data) return; const { content, toc } = await loadMdx(data.originalPath || data.relativePath); if (cancelled.current) return; setErrorStatus(null); - setPage({ slug: route.slug, frontmatter: data.frontmatter, content, toc }); + setPage({ slug: route.slug, frontmatter: data.frontmatter, content, toc, prev: data.prev, next: data.next }); }) .catch(() => { if (!cancelled.current) { diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index e1710b4..35ce71f 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -32,12 +32,7 @@ export function DocsPage({ slug }: DocsPageProps) { }} /> diff --git a/packages/chronicle/src/server/entry-client.tsx b/packages/chronicle/src/server/entry-client.tsx index 141ae5f..f862e89 100644 --- a/packages/chronicle/src/server/entry-client.tsx +++ b/packages/chronicle/src/server/entry-client.tsx @@ -8,7 +8,7 @@ import { getApiConfigsForVersion } from '@/lib/config'; import { PageProvider } from '@/lib/page-context'; import { resolveRoute, RouteType } from '@/lib/route-resolver'; import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source'; -import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types'; +import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types'; import type { ApiSpec } from '@/lib/openapi'; import type { ReactNode } from 'react'; import { App } from './App'; @@ -21,6 +21,8 @@ interface EmbeddedData { frontmatter: Frontmatter; relativePath: string; originalPath?: string; + prev: PageNavLink | null; + next: PageNavLink | null; } const defaultConfig: ChronicleConfig = { @@ -83,6 +85,8 @@ async function hydrate() { ? { slug: embedded!.slug, frontmatter: embedded!.frontmatter, + prev: embedded!.prev, + next: embedded!.next, ...(await loadMdxModule(mdxPath)), } : null; diff --git a/packages/chronicle/src/server/entry-server.tsx b/packages/chronicle/src/server/entry-server.tsx index e5721d7..6536a88 100644 --- a/packages/chronicle/src/server/entry-server.tsx +++ b/packages/chronicle/src/server/entry-server.tsx @@ -58,6 +58,8 @@ export default { ? React.createElement(mdxModule.default, { components: mdxComponents }) : null, toc: mdxModule?.toc ?? [], + prev: null, + next: null, } : null; @@ -69,6 +71,8 @@ export default { frontmatter: pageData?.frontmatter ?? null, relativePath, originalPath, + prev: pageData?.prev ?? null, + next: pageData?.next ?? null, }; const safeJson = JSON.stringify(embeddedData).replace(/ Date: Wed, 22 Apr 2026 10:47:37 +0530 Subject: [PATCH 02/27] fix: add std-env as root dep to unblock nitro beta nitro@3.0.260311-beta lists std-env in devDependencies but its runtime imports it. Node ESM resolution from nitro's isolated cache path fails without std-env at top-level node_modules. Co-Authored-By: Claude Opus 4.7 (1M context) --- bun.lock | 3 +++ package.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/bun.lock b/bun.lock index 84ca123..5a1a84d 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "chronicle", + "dependencies": { + "std-env": "^4.0.0", + }, }, "packages/chronicle": { "name": "@raystack/chronicle", diff --git a/package.json b/package.json index 5ccda8b..c433f52 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "build:examples:basic": "./packages/chronicle/bin/chronicle.js build --config examples/basic/chronicle.yaml", "dev:examples:versioned": "./packages/chronicle/bin/chronicle.js dev --config examples/versioned/chronicle.yaml", "build:examples:versioned": "./packages/chronicle/bin/chronicle.js build --config examples/versioned/chronicle.yaml" + }, + "dependencies": { + "std-env": "^4.0.0" } } From 1d8388460a841efda300b22bdcd18bed4c6fb102 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 22 Apr 2026 11:05:43 +0530 Subject: [PATCH 03/27] feat: restructure default theme layout with tab bar Drop the top Navbar; move navigation into a per-section tab bar alongside the sidebar. Resize sidebar to 262px, swap button-based nav links for pill-style tabs using mini typography tokens. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/ui/search.module.css | 13 --- .../chronicle/src/components/ui/search.tsx | 37 +++----- .../src/themes/default/Layout.module.css | 86 ++++++++++--------- .../chronicle/src/themes/default/Layout.tsx | 82 ++++++++---------- 4 files changed, 95 insertions(+), 123 deletions(-) diff --git a/packages/chronicle/src/components/ui/search.module.css b/packages/chronicle/src/components/ui/search.module.css index 086e7f4..6a6b4bf 100644 --- a/packages/chronicle/src/components/ui/search.module.css +++ b/packages/chronicle/src/components/ui/search.module.css @@ -1,16 +1,3 @@ -.trigger { - gap: 8px; - color: var(--rs-color-foreground-base-secondary); - cursor: pointer; -} - -.kbd { - padding: 2px 6px; - border-radius: 4px; - border: 1px solid var(--rs-color-border-base-primary); - font-size: 12px; -} - .dialogContent { max-width: 600px; padding: 0; diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index 370c4ce..e2e0d21 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -1,6 +1,9 @@ -import { DocumentIcon, HashtagIcon } from '@heroicons/react/24/outline'; -import { Button, Command, Dialog, Text } from '@raystack/apsara'; -import { cx } from 'class-variance-authority'; +import { + DocumentIcon, + HashtagIcon, + MagnifyingGlassIcon +} from '@heroicons/react/24/outline'; +import { Command, Dialog, IconButton, Text } from '@raystack/apsara'; import type { SortedResult } from 'fumadocs-core/search'; import { useDocsSearch } from 'fumadocs-core/search/client'; import { useCallback, useEffect, useState } from 'react'; @@ -9,21 +12,6 @@ import { MethodBadge } from '@/components/api/method-badge'; import { usePageContext } from '@/lib/page-context'; import styles from './search.module.css'; -function SearchShortcutKey({ className }: { className?: string }) { - const [key, setKey] = useState('⌘'); - - useEffect(() => { - const isMac = navigator.platform?.toUpperCase().includes('MAC'); - setKey(isMac ? '⌘' : 'Ctrl'); - }, []); - - return ( - - {key} K - - ); -} - interface SearchProps { className?: string; } @@ -67,16 +55,13 @@ export function Search({ className }: SearchProps) { return ( <> - + + diff --git a/packages/chronicle/src/themes/default/Layout.module.css b/packages/chronicle/src/themes/default/Layout.module.css index dadbe5d..52b55c8 100644 --- a/packages/chronicle/src/themes/default/Layout.module.css +++ b/packages/chronicle/src/themes/default/Layout.module.css @@ -2,76 +2,84 @@ min-height: 100vh; } -.header { - border-bottom: 1px solid var(--rs-color-border-base-primary); -} - -.search { - margin-left: var(--rs-space-5); -} - .body { flex: 1; } .sidebar { - width: 260px; + width: 262px; position: sticky; top: 0; height: 100vh; } -.content { - flex: 1; - padding: var(--rs-space-9); -} - -.sidebarList { - list-style: none; - padding: 0; - margin: 0; +.sidebarHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--rs-space-3); + padding: var(--rs-space-3) var(--rs-space-4); } -.separator { - height: 1px; +.headerDivider { + width: 1px; + height: 13px; background: var(--rs-color-border-base-primary); - margin: var(--rs-space-3) 0; } -.folder { - margin-bottom: var(--rs-space-3); +.mainArea { + flex: 1; + min-width: 0; } -.folderLabel { - font-weight: 500; - font-size: 0.875rem; - color: var(--rs-color-text-base-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; +.tabBar { + display: flex; + align-items: center; + height: 48px; + padding: 0 var(--rs-space-2); + background: var(--rs-color-background-base-secondary); + border-bottom: 1px solid var(--rs-color-border-base-primary); } -.folder > .sidebarList { - margin-top: var(--rs-space-2); - padding-left: var(--rs-space-4); +.tabs { + display: flex; + align-items: center; + gap: var(--rs-space-3); } -.navButton { +.tab { display: flex; align-items: center; - height: 32px; - padding: 0 var(--rs-space-4); - border: 1px solid var(--rs-color-border-base-primary); + gap: var(--rs-space-2); + padding: var(--rs-space-2) var(--rs-space-3); + border: 0.5px solid var(--rs-color-border-base-primary); border-radius: var(--rs-radius-2); - font-size: var(--rs-font-size-small); + font-size: var(--rs-font-size-mini); font-weight: var(--rs-font-weight-medium); - color: var(--rs-color-foreground-base-primary); + letter-spacing: var(--rs-letter-spacing-mini); + line-height: var(--rs-line-height-mini); + color: var(--rs-color-foreground-base-secondary); text-decoration: none; + white-space: nowrap; + cursor: pointer; } -.navButton:hover { +.tab:hover { + color: var(--rs-color-foreground-base-primary); background: var(--rs-color-background-base-primary-hover); } +.tabActive { + background: var(--rs-color-background-neutral-primary); + border-color: var(--rs-color-border-base-secondary); + color: var(--rs-color-foreground-base-primary); +} + +.content { + flex: 1; + padding: var(--rs-space-9); +} + .groupItems { padding-left: var(--rs-space-4); } diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 9017c61..b56688e 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -1,12 +1,9 @@ -import { RectangleStackIcon } from '@heroicons/react/24/outline'; import { - Button, - Flex, - Headline, - Link, - Navbar, - Sidebar -} from '@raystack/apsara'; + CodeBracketSquareIcon, + CubeIcon, + RectangleStackIcon +} from '@heroicons/react/24/outline'; +import { Flex, Sidebar } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; import { useEffect, useRef } from 'react'; import { Link as RouterLink, useLocation } from 'react-router'; @@ -40,6 +37,7 @@ export function Layout({ }: ThemeLayoutProps) { const { pathname } = useLocation(); const scrollRef = useRef(null); + const isApiRoute = pathname.startsWith('/apis'); useEffect(() => { const el = scrollRef.current; @@ -61,40 +59,6 @@ export function Layout({ return ( - - - - - {config.site.title} - - - - - - - - {config.api?.map(api => ( - - {api.name} API - - ))} - {config.navigation?.links?.map(link => ( - - {link.label} - - ))} - {config.search?.enabled && } - - - - {hideSidebar ? null : ( + + + +
+ + + + {config.search?.enabled && } + + + {tree.children.map((item, i) => ( )} -
- {children} -
+ + +
+ {children} +
+
From 664a820813df05d4766e83451bbd237e0b51a554 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Fri, 17 Apr 2026 11:46:29 +0530 Subject: [PATCH 04/27] chore: switch docs theme to default Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/chronicle.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/chronicle.yaml b/docs/chronicle.yaml index 627ae8f..09efcbf 100644 --- a/docs/chronicle.yaml +++ b/docs/chronicle.yaml @@ -7,7 +7,7 @@ content: label: Docs theme: - name: paper + name: default navigation: links: From c51f3fa78a6ecc242311949fa9e6055b7a42abc7 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 22 Apr 2026 11:08:18 +0530 Subject: [PATCH 05/27] feat: compute page prev/next on the server Add getPageNav helper that flattens the page tree and returns the adjacent PageNavLinks for a slug. Both the /api/page handler and SSR pageData now populate prev/next so the client can render navigation without re-flattening the tree. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/chronicle/src/lib/source.ts | 19 ++++++++++++++++++- packages/chronicle/src/server/api/page.ts | 6 +++++- .../chronicle/src/server/entry-server.tsx | 9 +++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/chronicle/src/lib/source.ts b/packages/chronicle/src/lib/source.ts index e528c8d..0f74c10 100644 --- a/packages/chronicle/src/lib/source.ts +++ b/packages/chronicle/src/lib/source.ts @@ -1,8 +1,8 @@ import { loader } from 'fumadocs-core/source'; +import { flattenTree } from 'fumadocs-core/page-tree'; import type { Root, Node, Folder } from 'fumadocs-core/page-tree'; import type { MDXContent } from 'mdx/types'; import type { TableOfContents } from 'fumadocs-core/toc'; -import type { Frontmatter } from '@/types'; import { getLatestContentRoots, getVersionContentRoots, @@ -14,6 +14,7 @@ import { resolveVersionFromUrl, type VersionContext, } from './version-source'; +import type { Frontmatter, PageNav, PageNavLink } from '@/types'; const CONTENT_PREFIX = '../../.content/'; @@ -168,6 +169,22 @@ export function getVersionContextForUrl(url: string): VersionContext { export type { VersionContext } from './version-source'; +export async function getPageNav(slug: string[]): Promise { + const tree = await getPageTree(); + const pages = flattenTree(tree.children); + const url = slug.length === 0 ? '/' : `/${slug.join('/')}`; + const i = pages.findIndex(p => p.url === url); + if (i < 0) return { prev: null, next: null }; + const toLink = (p: (typeof pages)[number]): PageNavLink => ({ + url: p.url, + title: String(p.name ?? '') + }); + return { + prev: i > 0 ? toLink(pages[i - 1]) : null, + next: i < pages.length - 1 ? toLink(pages[i + 1]) : null + }; +} + export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter { const d = page.data as Record; return { diff --git a/packages/chronicle/src/server/api/page.ts b/packages/chronicle/src/server/api/page.ts index bfb43aa..568d6aa 100644 --- a/packages/chronicle/src/server/api/page.ts +++ b/packages/chronicle/src/server/api/page.ts @@ -1,5 +1,5 @@ import { defineHandler, HTTPError } from 'nitro'; -import { getPage, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; +import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; export default defineHandler(async event => { const slugParam = event.url.searchParams.get('slug') ?? ''; @@ -10,9 +10,13 @@ export default defineHandler(async event => { throw new HTTPError({ status: 404, message: 'Page not found' }); } + const nav = await getPageNav(slug); + return { frontmatter: extractFrontmatter(page, slug[slug.length - 1]), relativePath: getRelativePath(page), originalPath: getOriginalPath(page), + prev: nav.prev, + next: nav.next, }; }); diff --git a/packages/chronicle/src/server/entry-server.tsx b/packages/chronicle/src/server/entry-server.tsx index 6536a88..0fa6b2a 100644 --- a/packages/chronicle/src/server/entry-server.tsx +++ b/packages/chronicle/src/server/entry-server.tsx @@ -9,7 +9,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; import { PageProvider } from '@/lib/page-context'; import { resolveRoute, RouteType } from '@/lib/route-resolver'; -import { getPage, getPageTree, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; +import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; import { useNitroApp } from 'nitro/app'; import { App } from './App'; @@ -41,9 +41,10 @@ export default { : []; const apiSpecs = apiConfigs.length ? await loadApiSpecs(apiConfigs) : []; - const [tree, page] = await Promise.all([ + const [tree, page, nav] = await Promise.all([ getPageTree(), route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null), + getPageNav(pageSlug), ]); const relativePath = page ? getRelativePath(page) : null; @@ -58,8 +59,8 @@ export default { ? React.createElement(mdxModule.default, { components: mdxComponents }) : null, toc: mdxModule?.toc ?? [], - prev: null, - next: null, + prev: nav.prev, + next: nav.next, } : null; From e589e498e4d6dbcfb0f8dcdd48e828eae2f8ee94 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Wed, 22 Apr 2026 11:09:42 +0530 Subject: [PATCH 06/27] feat: wrap content in card with prev/next sub-navbar Default theme now renders the main content inside a rounded, bordered card with a sub-navbar at the top containing prev/next IconButtons driven by the server-provided page nav, followed by breadcrumbs. Breadcrumbs move out of Page.tsx since the Layout owns the chrome. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/themes/default/Layout.module.css | 37 ++++++++++++- .../chronicle/src/themes/default/Layout.tsx | 53 ++++++++++++++++--- .../chronicle/src/themes/default/Page.tsx | 4 +- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/packages/chronicle/src/themes/default/Layout.module.css b/packages/chronicle/src/themes/default/Layout.module.css index 52b55c8..405360e 100644 --- a/packages/chronicle/src/themes/default/Layout.module.css +++ b/packages/chronicle/src/themes/default/Layout.module.css @@ -75,9 +75,44 @@ color: var(--rs-color-foreground-base-primary); } +.cardWrapper { + flex: 1; + display: flex; + padding: 0 var(--rs-space-2) var(--rs-space-2); + min-height: 0; +} + +.card { + flex: 1; + display: flex; + flex-direction: column; + background: var(--rs-color-background-base-primary); + border: 0.5px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-4); + box-shadow: + 0 1px 2px 0 rgba(0, 0, 0, 0.04), + 0 2px 4px 0 rgba(0, 0, 0, 0.04); + overflow: clip; +} + +.subNav { + display: flex; + align-items: center; + justify-content: space-between; + height: 48px; + padding: var(--rs-space-4) var(--rs-space-7); + background: var(--rs-color-background-base-primary); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + backdrop-filter: blur(1px); +} + +.subNavLeft { + min-width: 0; +} + .content { flex: 1; - padding: var(--rs-space-9); + padding: var(--rs-space-9) var(--rs-space-7); } .groupItems { diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index b56688e..b125453 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -1,16 +1,20 @@ import { - CodeBracketSquareIcon, + ArrowLeftIcon, CubeIcon, + ArrowRightIcon, + CodeBracketSquareIcon, RectangleStackIcon } from '@heroicons/react/24/outline'; -import { Flex, Sidebar } from '@raystack/apsara'; +import { Flex, IconButton, Sidebar } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; -import { useEffect, useRef } from 'react'; -import { Link as RouterLink, useLocation } from 'react-router'; +import { useEffect, useMemo, useRef } from 'react'; +import { Link as RouterLink, useLocation, useNavigate } from 'react-router'; import { MethodBadge } from '@/components/api/method-badge'; import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher'; import { Footer } from '@/components/ui/footer'; import { Search } from '@/components/ui/search'; +import { Breadcrumbs } from '@/components/ui/breadcrumbs'; +import { usePageContext } from '@/lib/page-context'; import type { Node } from 'fumadocs-core/page-tree'; import type { ThemeLayoutProps } from '@/types'; import { ContentDirButtons } from './ContentDirButtons'; @@ -36,8 +40,16 @@ export function Layout({ classNames }: ThemeLayoutProps) { const { pathname } = useLocation(); + const navigate = useNavigate(); + const { page } = usePageContext(); const scrollRef = useRef(null); const isApiRoute = pathname.startsWith('/apis'); + const { prev, next } = page ?? { prev: null, next: null }; + + const slug = useMemo( + () => (pathname === '/' ? [] : pathname.split('/').filter(Boolean)), + [pathname] + ); useEffect(() => { const el = scrollRef.current; @@ -104,9 +116,36 @@ export function Layout({ ))}
-
- {children} -
+
+
+ +
+ {children} +
+
+