Rework docs navigation and move alternatives to top-level pages (#1487)
* Rework docs navigation and move alternatives to top-level pages - Add nested docs nav with categories, collapsible groups, breadcrumbs, and a right-hand page outline with heading slugs. - Move alternatives out of /docs/alternatives into /alternatives/* marketing pages with 301 redirects. - Add github-slugger for heading IDs and require explicit width on SiteShell so new pages can't accidentally pick the narrow prose layout. - Remove the best-practices doc from public-docs. * Make docs content column prose-width
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/claude-desktop")(
|
||||
alternativeRouteOptions("claude-desktop"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/codex-app")(
|
||||
alternativeRouteOptions("codex-app"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/conductor")(
|
||||
alternativeRouteOptions("conductor"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/happy-coder")(
|
||||
alternativeRouteOptions("happy-coder"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/openchamber")(
|
||||
alternativeRouteOptions("openchamber"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/opencode-desktop")(
|
||||
alternativeRouteOptions("opencode-desktop"),
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/superset")(alternativeRouteOptions("superset"));
|
||||
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/blog")({
|
||||
|
||||
function BlogLayout() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<Outlet />
|
||||
</SiteShell>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Route = createFileRoute("/changelog")({
|
||||
|
||||
function Changelog() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
@@ -22,7 +22,7 @@ type Status = "idle" | "submitting" | "success" | "error";
|
||||
|
||||
function Cloud() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl font-medium mb-3">Paseo Cloud</h1>
|
||||
<p className="text-white/70 leading-relaxed mb-10">
|
||||
For using Paseo across machines, with a team, or inside a company. Looking for design
|
||||
|
||||
@@ -1,55 +1,23 @@
|
||||
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
|
||||
import { createFileRoute, Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { type Doc, getDocs } from "~/docs";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { DocsBreadcrumbs } from "~/components/docs-breadcrumbs";
|
||||
import { DocsNav } from "~/components/docs-nav";
|
||||
import { DocsOutline } from "~/components/docs-outline";
|
||||
import { buildDocsNavTree, getDoc, getDocs } from "~/docs";
|
||||
import "~/styles.css";
|
||||
|
||||
export const Route = createFileRoute("/docs")({
|
||||
component: DocsLayout,
|
||||
});
|
||||
|
||||
const ACTIVE_OPTIONS_EXACT = { exact: true };
|
||||
const MOBILE_ACTIVE_PROPS = { className: "text-foreground" };
|
||||
const DESKTOP_ACTIVE_PROPS = { className: "bg-muted text-foreground" };
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface NavGroup {
|
||||
name: string | null;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
alternatives: "Alternatives",
|
||||
};
|
||||
|
||||
function groupKey(doc: Doc): string | null {
|
||||
const idx = doc.slug.indexOf("/");
|
||||
return idx === -1 ? null : doc.slug.slice(0, idx);
|
||||
}
|
||||
|
||||
function buildNavigation(): NavGroup[] {
|
||||
const groups = new Map<string | null, NavItem[]>();
|
||||
for (const doc of getDocs()) {
|
||||
const key = groupKey(doc);
|
||||
const items = groups.get(key) ?? [];
|
||||
items.push({ name: doc.frontmatter.nav, href: doc.href });
|
||||
groups.set(key, items);
|
||||
}
|
||||
const ordered: NavGroup[] = [];
|
||||
if (groups.has(null)) ordered.push({ name: null, items: groups.get(null)! });
|
||||
for (const [key, items] of groups) {
|
||||
if (key === null) continue;
|
||||
ordered.push({ name: GROUP_LABELS[key] ?? key, items });
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
function DocsLayout() {
|
||||
const groups = buildNavigation();
|
||||
const location = useLocation();
|
||||
const tree = useMemo(() => buildDocsNavTree(getDocs()), []);
|
||||
|
||||
const slug = location.pathname === "/docs" ? "" : location.pathname.slice("/docs/".length);
|
||||
const doc = getDoc(slug);
|
||||
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
const toggleMobileNav = useCallback(() => setMobileNavOpen((v) => !v), []);
|
||||
const closeMobileNav = useCallback(() => setMobileNavOpen(false), []);
|
||||
@@ -57,7 +25,7 @@ function DocsLayout() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Mobile header */}
|
||||
<header className="md:hidden sticky top-0 z-50 bg-background border-b border-border">
|
||||
<header className="lg:hidden sticky top-0 z-50 bg-background border-b border-border">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||
@@ -74,61 +42,34 @@ function DocsLayout() {
|
||||
</button>
|
||||
</div>
|
||||
{mobileNavOpen && (
|
||||
<nav className="border-t border-border px-4 py-4 space-y-4 max-h-[calc(100dvh-4rem)] overflow-y-auto">
|
||||
{groups.map((group) => (
|
||||
<div key={group.name ?? "root"} className="space-y-1">
|
||||
{group.name && (
|
||||
<div className="text-sm font-medium text-foreground mb-2">{group.name}</div>
|
||||
)}
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
onClick={closeMobileNav}
|
||||
className="block py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
activeProps={MOBILE_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<nav className="border-t border-border px-4 py-4 max-h-[calc(100dvh-4rem)] overflow-y-auto">
|
||||
<DocsNav nodes={tree} mobile onNavigate={closeMobileNav} />
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="flex">
|
||||
<div className="max-w-[90rem] mx-auto flex items-start">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex md:flex-col w-56 shrink-0 border-r border-border p-6 sticky top-0 h-screen">
|
||||
<Link to="/" className="flex items-center gap-3 mb-8 shrink-0">
|
||||
<aside className="hidden lg:block sticky top-0 h-screen w-60 shrink-0 border-r border-border p-6 overflow-y-auto">
|
||||
<Link to="/" className="flex items-center gap-3 mb-8">
|
||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||
<span className="text-lg font-medium">Paseo</span>
|
||||
</Link>
|
||||
<nav className="flex-1 min-h-0 overflow-y-auto -ml-3 -mr-3 pr-3 space-y-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.name ?? "root"} className="space-y-1">
|
||||
{group.name && (
|
||||
<div className="px-3 py-2 text-sm font-medium text-foreground">{group.name}</div>
|
||||
)}
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
className="block px-3 py-2 text-sm rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
activeProps={DESKTOP_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<DocsNav nodes={tree} />
|
||||
</aside>
|
||||
<main className="flex-1 min-w-0 p-6 md:p-12 max-w-3xl docs-prose">
|
||||
<Outlet />
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-w-0 px-6 md:px-12 py-8 md:py-12">
|
||||
<div className="max-w-prose mx-auto">
|
||||
{doc && <DocsBreadcrumbs doc={doc} tree={tree} />}
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right outline */}
|
||||
<aside className="hidden xl:block sticky top-0 h-screen w-60 shrink-0 px-2 overflow-y-auto">
|
||||
{doc && <DocsOutline headings={doc.headings} />}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ function Download() {
|
||||
const urls = downloadUrls(release);
|
||||
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
|
||||
<p className="text-muted-foreground mb-10">v{version}</p>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/privacy")({
|
||||
|
||||
function Privacy() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
|
||||
|
||||
<div className="space-y-6 text-white/70 leading-relaxed">
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/sponsor")({
|
||||
|
||||
function Sponsor() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl font-medium tracking-tight mb-8">Sponsor</h1>
|
||||
|
||||
{/* Founder note */}
|
||||
|
||||
Reference in New Issue
Block a user