From 2b24528cbb3352d035e0ef7be32165034c47c187 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:52:54 +0000 Subject: [PATCH 1/3] fix(docs): keep wide tables from widening the page on mobile Wrap props tables in a horizontally scrollable container and allow long type names in API item headings to wrap, so the Components, Utilities and Types pages no longer render zoomed out on narrow screens. Closes #262 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HFdmEE6GBwx33cNXgjcv7d --- packages/docs/src/pages/ApiComponentsPage.tsx | 186 +++++++------- packages/docs/src/pages/ApiUtilitiesPage.tsx | 232 +++++++++--------- packages/docs/src/styles/components.css | 8 +- 3 files changed, 219 insertions(+), 207 deletions(-) diff --git a/packages/docs/src/pages/ApiComponentsPage.tsx b/packages/docs/src/pages/ApiComponentsPage.tsx index 67dc4ba..fa5b086 100644 --- a/packages/docs/src/pages/ApiComponentsPage.tsx +++ b/packages/docs/src/pages/ApiComponentsPage.tsx @@ -27,98 +27,100 @@ export function ApiComponentsPage() { }} />`}

Props

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropTypeDescription
- routes - - RouteDefinition[] - Array of route definitions
- onNavigate - - OnNavigateCallback - - Callback fired before navigation is intercepted. Receives the NavigateEvent and an - info object with matched routes and whether the router will intercept the - navigation. -
- fallback - - {'"none" | "static"'} - - Fallback mode when Navigation API is unavailable. "none" (default) - renders nothing; "static" renders matched routes using{" "} - window.location without navigation interception (MPA behavior). -
- trailingSlash - - {'"ignore" | "strict"'} - - How trailing slashes are treated during route matching. "ignore"{" "} - (default) ignores a single trailing slash on the pathname or on route{" "} - path patterns, so /users/ matches{" "} - path: "/users"; the URL itself is never rewritten.{" "} - "strict" requires pathnames to match patterns exactly. -
- ssr - - SSRConfig - - SSR configuration for route matching during server-side rendering. Accepts an object - with path (the pathname to match against) and an optional{" "} - runLoaders boolean (defaults to false - ). When runLoaders is false, routes with loaders are - skipped during SSR. Once the client hydrates, the real URL from the Navigation API - takes over. See the SSR guide for details. -
- features - - RouterFeatures - - Opt-in feature flags that change the Router's behavior. Currently supports{" "} - pathlessSSROutletDeferral, which uses React's browser(){" "} - API (React 19.3+) to defer unmatched outlet content to the browser during pathless - SSR (this will become the default behavior in the next major version). See the{" "} - SSR guide for details. -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropTypeDescription
+ routes + + RouteDefinition[] + Array of route definitions
+ onNavigate + + OnNavigateCallback + + Callback fired before navigation is intercepted. Receives the NavigateEvent and an + info object with matched routes and whether the router will intercept the + navigation. +
+ fallback + + {'"none" | "static"'} + + Fallback mode when Navigation API is unavailable. "none" (default) + renders nothing; "static" renders matched routes using{" "} + window.location without navigation interception (MPA behavior). +
+ trailingSlash + + {'"ignore" | "strict"'} + + How trailing slashes are treated during route matching. "ignore"{" "} + (default) ignores a single trailing slash on the pathname or on route{" "} + path patterns, so /users/ matches{" "} + path: "/users"; the URL itself is never rewritten.{" "} + "strict" requires pathnames to match patterns exactly. +
+ ssr + + SSRConfig + + SSR configuration for route matching during server-side rendering. Accepts an + object with path (the pathname to match against) and an optional{" "} + runLoaders boolean (defaults to false + ). When runLoaders is false, routes with loaders are + skipped during SSR. Once the client hydrates, the real URL from the Navigation API + takes over. See the SSR guide for details. +
+ features + + RouterFeatures + + Opt-in feature flags that change the Router's behavior. Currently supports{" "} + pathlessSSROutletDeferral, which uses React's browser(){" "} + API (React 19.3+) to defer unmatched outlet content to the browser during pathless + SSR (this will become the default behavior in the next major version). See the{" "} + SSR guide for details. +
+
diff --git a/packages/docs/src/pages/ApiUtilitiesPage.tsx b/packages/docs/src/pages/ApiUtilitiesPage.tsx index dbae7d5..d012317 100644 --- a/packages/docs/src/pages/ApiUtilitiesPage.tsx +++ b/packages/docs/src/pages/ApiUtilitiesPage.tsx @@ -53,100 +53,102 @@ const myRoute = route({ ], });`}

Options

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OptionTypeDescription
- path - - string (optional) - - URL path pattern (supports :param syntax). If omitted, creates a - pathless route that always matches and consumes no pathname. Useful for layout - wrappers. -
- component - - ComponentType - - React component to render. Receives params prop (and data{" "} - prop if loader is defined) -
- action - - (args: ActionArgs) => T - - Function to handle form submissions (POST navigations). Receives a{" "} - Request with FormData body. The return value is passed to - the loader as actionResult. -
- loader - - (args: LoaderArgs) => T - Function to load data. May be synchronous or asynchronous
- children - - RouteDefinition[] - Nested child routes
- exact - - boolean - - Override default matching. true = exact match only, false{" "} - = prefix match. Defaults to true for leaf routes, false{" "} - for parent routes. -
- requireChildren - - boolean - - Whether a parent route requires a child to match. true (default) = - parent only matches if a child matches, false = parent can match alone - with outlet as null. Enables catch-all routes to work - intuitively. -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDescription
+ path + + string (optional) + + URL path pattern (supports :param syntax). If omitted, creates a + pathless route that always matches and consumes no pathname. Useful for layout + wrappers. +
+ component + + ComponentType + + React component to render. Receives params prop (and{" "} + data prop if loader is defined) +
+ action + + (args: ActionArgs) => T + + Function to handle form submissions (POST navigations). Receives a{" "} + Request with FormData body. The return value is passed + to the loader as actionResult. +
+ loader + + (args: LoaderArgs) => T + Function to load data. May be synchronous or asynchronous
+ children + + RouteDefinition[] + Nested child routes
+ exact + + boolean + + Override default matching. true = exact match only,{" "} + false = prefix match. Defaults to true for leaf routes,{" "} + false for parent routes. +
+ requireChildren + + boolean + + Whether a parent route requires a child to match. true (default) = + parent only matches if a child matches, false = parent can match + alone with outlet as null. Enables catch-all routes to + work intuitively. +
+
@@ -283,26 +285,28 @@ hardReload();`} // Full page navigation — bypasses the router and all blockers hardNavigate("/other-page");`}

Parameters

- - - - - - - - - - - - - - - -
ParameterTypeDescription
- url - - string - The URL to navigate to
+
+ + + + + + + + + + + + + + + +
ParameterTypeDescription
+ url + + string + The URL to navigate to
+
diff --git a/packages/docs/src/styles/components.css b/packages/docs/src/styles/components.css index e15260d..c176267 100644 --- a/packages/docs/src/styles/components.css +++ b/packages/docs/src/styles/components.css @@ -64,10 +64,15 @@ } /* Props Table */ +/* Wide tables scroll within this container instead of widening the page */ +.table-scroll { + overflow-x: auto; + margin: 16px 0; +} + .props-table { width: 100%; border-collapse: collapse; - margin: 16px 0; } .props-table th, @@ -131,6 +136,7 @@ } .api-item h3 code { + overflow-wrap: anywhere; font-size: 1.1rem; background: var(--color-bg-secondary); padding: 4px 12px; From 131709d60fd004d96cbe233f6b5bc963cb11b469 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 09:52:54 +0000 Subject: [PATCH 2/3] feat(docs): add a site-wide documentation sidebar Documentation pages now show a sticky sidebar listing all pages grouped by section, with the current page highlighted. On narrow screens the same navigation (including a GitHub link) is available from the hamburger menu. Closes #261 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HFdmEE6GBwx33cNXgjcv7d --- packages/docs/src/App.tsx | 240 ++++++++++--------- packages/docs/src/components/DocsLayout.tsx | 14 ++ packages/docs/src/components/DocsNav.tsx | 84 +++++++ packages/docs/src/components/DocsSidebar.tsx | 32 +++ packages/docs/src/components/Header.tsx | 30 +-- packages/docs/src/navigation.ts | 84 +++++++ packages/docs/src/pages/ApiReferencePage.tsx | 8 +- packages/docs/src/pages/LearnPage.tsx | 45 +--- packages/docs/src/styles/layout.css | 71 ++++++ packages/docs/src/styles/responsive.css | 20 ++ 10 files changed, 446 insertions(+), 182 deletions(-) create mode 100644 packages/docs/src/components/DocsLayout.tsx create mode 100644 packages/docs/src/components/DocsNav.tsx create mode 100644 packages/docs/src/components/DocsSidebar.tsx create mode 100644 packages/docs/src/navigation.ts 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 && ( -