diff --git a/packages/docs/src/App.tsx b/packages/docs/src/App.tsx index c47c3f0..40a0cae 100644 --- a/packages/docs/src/App.tsx +++ b/packages/docs/src/App.tsx @@ -3,6 +3,7 @@ import { PageMeta } from "./pageMeta.js"; import { route } from "@funstack/router/server"; import { defer } from "@funstack/static/server"; import { Layout } from "./components/Layout.js"; +import { DocsLayout } from "./components/DocsLayout.js"; import { ApiReferencePage } from "./pages/ApiReferencePage.js"; import { LearnPage } from "./pages/LearnPage.js"; import { ClientApp } from "./ClientApp.js"; @@ -142,160 +143,171 @@ export const routes = [ component: defer(withMeta("/", ), { name: "HomePage" }), }), route({ - path: "/getting-started", - component: defer(withMeta("/getting-started", ), { - name: "GettingStartedPage", - }), - }), - route({ - path: "/learn", - component: LearnPage, + component: , children: [ route({ - path: "/", - component: defer(withMeta("/learn", ), { - name: "LearnIndexPage", - }), - }), - route({ - path: "/navigation-api", - component: defer(withMeta("/learn/navigation-api", ), { - name: "LearnNavigationApiPage", + path: "/getting-started", + component: defer(withMeta("/getting-started", ), { + name: "GettingStartedPage", }), }), route({ - path: "/nested-routes", - component: defer(withMeta("/learn/nested-routes", ), { - name: "LearnNestedRoutesPage", - }), - }), - route({ - path: "/type-safety", - component: defer(withMeta("/learn/type-safety", ), { - name: "LearnTypeSafetyPage", - }), - }), - route({ - path: "/ssr", - component: , + path: "/learn", + component: LearnPage, children: [ route({ path: "/", - component: defer(withMeta("/learn/ssr", ), { - name: "LearnSsrBasicPage", + component: defer(withMeta("/learn", ), { + name: "LearnIndexPage", }), }), route({ - path: "/static-site-generation", + path: "/navigation-api", component: defer( - withMeta("/learn/ssr/static-site-generation", ), + withMeta("/learn/navigation-api", ), { - name: "LearnSsgPage", + name: "LearnNavigationApiPage", }, ), }), route({ - path: "/with-loaders", + path: "/nested-routes", + component: defer(withMeta("/learn/nested-routes", ), { + name: "LearnNestedRoutesPage", + }), + }), + route({ + path: "/type-safety", + component: defer(withMeta("/learn/type-safety", ), { + name: "LearnTypeSafetyPage", + }), + }), + route({ + path: "/ssr", + component: , + children: [ + route({ + path: "/", + component: defer(withMeta("/learn/ssr", ), { + name: "LearnSsrBasicPage", + }), + }), + route({ + path: "/static-site-generation", + component: defer( + withMeta("/learn/ssr/static-site-generation", ), + { + name: "LearnSsgPage", + }, + ), + }), + route({ + path: "/with-loaders", + component: defer( + withMeta("/learn/ssr/with-loaders", ), + { + name: "LearnSsrWithLoadersPage", + }, + ), + }), + ], + }), + route({ + path: "/rsc", + component: , + children: [ + route({ + path: "/", + component: defer(withMeta("/learn/rsc", ), { + name: "LearnRscPage", + }), + }), + route({ + path: "/route-features", + component: defer( + withMeta("/learn/rsc/route-features", ), + { + name: "LearnRouteDefinitionsPage", + }, + ), + }), + ], + }), + route({ + path: "/actions", + component: defer(withMeta("/learn/actions", ), { + name: "LearnActionsPage", + }), + }), + route({ + path: "/error-handling", component: defer( - withMeta("/learn/ssr/with-loaders", ), + withMeta("/learn/error-handling", ), { - name: "LearnSsrWithLoadersPage", + name: "LearnErrorHandlingPage", }, ), }), + route({ + path: "/transitions", + component: defer(withMeta("/learn/transitions", ), { + name: "LearnTransitionsPage", + }), + }), + route({ + path: "/loaders", + component: defer(withMeta("/learn/loaders", ), { + name: "LearnLoadersPage", + }), + }), ], }), route({ - path: "/rsc", - component: , + path: "/api", + component: ApiReferencePage, children: [ route({ path: "/", - component: defer(withMeta("/learn/rsc", ), { - name: "LearnRscPage", + component: defer(withMeta("/api", ), { + name: "ApiReferenceIndexPage", }), }), route({ - path: "/route-features", - component: defer( - withMeta("/learn/rsc/route-features", ), - { - name: "LearnRouteDefinitionsPage", - }, - ), + path: "/components", + component: defer(withMeta("/api/components", ), { + name: "ApiComponentsPage", + }), + }), + route({ + path: "/hooks", + component: defer(withMeta("/api/hooks", ), { + name: "ApiHooksPage", + }), + }), + route({ + path: "/utilities", + component: defer(withMeta("/api/utilities", ), { + name: "ApiUtilitiesPage", + }), + }), + route({ + path: "/types", + component: defer(withMeta("/api/types", ), { + name: "ApiTypesPage", + }), }), ], }), route({ - path: "/actions", - component: defer(withMeta("/learn/actions", ), { - name: "LearnActionsPage", - }), + path: "/examples", + component: defer(withMeta("/examples", ), { name: "ExamplesPage" }), }), route({ - path: "/error-handling", - component: defer(withMeta("/learn/error-handling", ), { - name: "LearnErrorHandlingPage", - }), - }), - route({ - path: "/transitions", - component: defer(withMeta("/learn/transitions", ), { - name: "LearnTransitionsPage", - }), - }), - route({ - path: "/loaders", - component: defer(withMeta("/learn/loaders", ), { - name: "LearnLoadersPage", - }), + path: "/faq", + component: defer(withMeta("/faq", ), { name: "FaqPage" }), }), ], }), - route({ - path: "/api", - component: ApiReferencePage, - children: [ - route({ - path: "/", - component: defer(withMeta("/api", ), { - name: "ApiReferenceIndexPage", - }), - }), - route({ - path: "/components", - component: defer(withMeta("/api/components", ), { - name: "ApiComponentsPage", - }), - }), - route({ - path: "/hooks", - component: defer(withMeta("/api/hooks", ), { - name: "ApiHooksPage", - }), - }), - route({ - path: "/utilities", - component: defer(withMeta("/api/utilities", ), { - name: "ApiUtilitiesPage", - }), - }), - route({ - path: "/types", - component: defer(withMeta("/api/types", ), { - name: "ApiTypesPage", - }), - }), - ], - }), - route({ - path: "/examples", - component: defer(withMeta("/examples", ), { name: "ExamplesPage" }), - }), - route({ - path: "/faq", - component: defer(withMeta("/faq", ), { name: "FaqPage" }), - }), route({ path: "/*", component: defer(withMeta("/*", ), { name: "NotFoundPage" }), diff --git a/packages/docs/src/components/DocsLayout.tsx b/packages/docs/src/components/DocsLayout.tsx new file mode 100644 index 0000000..28c0af6 --- /dev/null +++ b/packages/docs/src/components/DocsLayout.tsx @@ -0,0 +1,14 @@ +import { Outlet } from "@funstack/router"; +import { DocsSidebar } from "./DocsSidebar.js"; + +// Layout for documentation pages: a site-wide sidebar next to the page content. +export function DocsLayout() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/packages/docs/src/components/DocsNav.tsx b/packages/docs/src/components/DocsNav.tsx new file mode 100644 index 0000000..f59e774 --- /dev/null +++ b/packages/docs/src/components/DocsNav.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useLocation } from "@funstack/router"; +import { docsNavSections, isNavGroup, type NavItem } from "../navigation.js"; + +function DocsNavLink({ + item, + currentPath, + onNavigate, +}: { + item: NavItem; + currentPath: string; + onNavigate?: () => void; +}) { + if (item.external) { + return ( +
  • + + {item.label} + +
  • + ); + } + const isActive = currentPath === item.path; + return ( +
  • + + {item.label} + +
  • + ); +} + +// Lists all documentation pages grouped by section, highlighting the current page. +export function DocsNav({ onNavigate }: { onNavigate?: () => void }) { + const { pathname: currentPath } = useLocation(); + + return ( + <> + {docsNavSections.map((section) => ( +
    +

    {section.label}

    +
      + {section.items.map((entry) => + isNavGroup(entry) ? ( +
    • + {entry.label} +
        + {entry.items.map((item) => ( + + ))} +
      +
    • + ) : ( + + ), + )} +
    +
    + ))} + + ); +} diff --git a/packages/docs/src/components/DocsSidebar.tsx b/packages/docs/src/components/DocsSidebar.tsx new file mode 100644 index 0000000..a299cee --- /dev/null +++ b/packages/docs/src/components/DocsSidebar.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { DocsNav } from "./DocsNav.js"; + +export function DocsSidebar() { + const sidebarRef = useRef(null); + + // On initial load, scroll the sidebar so that the current page's link is visible. + // Later navigations keep the sidebar's scroll position since the user clicked a visible link. + useEffect(() => { + const sidebar = sidebarRef.current; + const activeLink = sidebar?.querySelector(".docs-nav-link.active"); + if (!sidebar || !activeLink) { + return; + } + const sidebarRect = sidebar.getBoundingClientRect(); + const linkRect = activeLink.getBoundingClientRect(); + if (linkRect.top < sidebarRect.top || linkRect.bottom > sidebarRect.bottom) { + sidebar.scrollTop += + linkRect.top - sidebarRect.top - (sidebarRect.height - linkRect.height) / 2; + } + }, []); + + return ( + + ); +} diff --git a/packages/docs/src/components/Header.tsx b/packages/docs/src/components/Header.tsx index 3dad82b..3e3b5ea 100644 --- a/packages/docs/src/components/Header.tsx +++ b/packages/docs/src/components/Header.tsx @@ -2,6 +2,8 @@ import { useLocation } from "@funstack/router"; import { useState } from "react"; +import { GITHUB_URL } from "../navigation.js"; +import { DocsNav } from "./DocsNav.js"; const navItems = [ { path: "/", label: "Home" }, @@ -51,34 +53,28 @@ export function Header() { ))} - + GitHub {isMenuOpen && ( -