Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions website/src/app/docs/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import { getDocBySlug, getDocSlugs, getAdjacentDocs, extractHeadings, stripCodeFences } from '@/lib/docs';
import { DOCS_SIDEBAR } from '@/lib/constants';
import { PROJECT } from '@/data/metrics';
import { Sidebar } from '@/components/docs/Sidebar';
import { Toc } from '@/components/docs/Toc';
import { DocNavigation } from '@/components/docs/DocNavigation';
Expand All @@ -18,6 +19,15 @@ function getReadingTime(text: string): number {
return Math.max(1, Math.ceil(words / 200));
}

function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
});
}

interface PageProps {
params: Promise<{ slug: string[] }>;
}
Expand Down Expand Up @@ -104,44 +114,57 @@ export default async function DocPage({ params }: PageProps) {
<>
<JsonLd data={[breadcrumbJsonLd, articleJsonLd]} />
<div className="mx-auto flex max-w-[90rem]">
{/* Sidebar */}
<div className="hidden lg:block w-64 shrink-0">
<Sidebar />
</div>
{/* Mobile sidebar */}
<div className="lg:hidden">
<Sidebar />
</div>
<div className="flex w-full flex-col lg:flex-row">
<Sidebar />

{/* Main content */}
<div className="min-w-0 flex-1 px-6 py-10 lg:px-12">
{/* Breadcrumbs */}
<nav aria-label="Breadcrumb" className="mb-6 text-sm text-zinc-300">
<ol className="flex items-center gap-2">
<li>
<ol className="flex min-w-0 items-center gap-2 whitespace-nowrap">
<li className="shrink-0">
<Link href="/docs" className="hover:text-white transition-colors">
Docs
</Link>
</li>
{categoryItem && (
<>
<li aria-hidden="true">/</li>
<li>{categoryItem.category}</li>
<li aria-hidden="true" className="shrink-0">/</li>
<li className="shrink-0">{categoryItem.category}</li>
</>
)}
<li aria-hidden="true">/</li>
<li className="text-zinc-300">{doc.title}</li>
<li aria-hidden="true" className="shrink-0">/</li>
<li className="min-w-0 truncate text-zinc-300" title={doc.label} aria-current="page">{doc.label}</li>
</ol>
</nav>

{/* Reading time */}
<div className="mb-6 flex items-center gap-2 text-sm text-text-muted">
{/* Meta: reading time, last updated, version, edit link */}
<div className="mb-6 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-text-muted">
<span className="flex items-center gap-2">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{readingTime} min read
</span>
<span>
Last updated{' '}
<time dateTime={doc.modifiedAt}>{formatDate(doc.modifiedAt)}</time>
</span>
<span className="rounded-full border border-white/10 px-2 py-0.5 text-xs text-zinc-400">
{PROJECT.version}
</span>
<a
href={`https://github.com/ajitpratap0/GoSQLX/edit/main/${doc.file}`}
target="_blank"
rel="noopener noreferrer"
className="text-zinc-400 underline-offset-2 transition-colors hover:text-white hover:underline"
>
Edit this page
</a>
</div>

<Toc headings={headings} variant="collapsible" />

{/* Content */}
<article className="prose prose-invert prose-dark max-w-none prose-code:before:content-none prose-code:after:content-none">
{content}
Expand All @@ -156,6 +179,7 @@ export default async function DocPage({ params }: PageProps) {
<Toc headings={headings} />
</div>
</div>
</div>
</>
);
}
30 changes: 30 additions & 0 deletions website/src/app/docs/search-index.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { DOCS_SIDEBAR } from '@/lib/constants';
import { extractHeadings, getDocBySlug } from '@/lib/docs';
import type { SearchEntry } from '@/lib/search-index';

// Generated once at build time; fetched lazily by the search modal.
export const dynamic = 'force-static';

export function GET() {
const entries: SearchEntry[] = [];

for (const group of DOCS_SIDEBAR) {
for (const item of group.items) {
const base = {
slug: item.slug,
title: item.label,
category: group.category,
description: item.description,
};
entries.push(base);

const doc = getDocBySlug(item.slug);
if (!doc) continue;
for (const h of extractHeadings(doc.content)) {
entries.push({ slug: base.slug, title: base.title, category: base.category, heading: h.text, anchor: h.id });
}
}
}

return Response.json(entries);
}
3 changes: 3 additions & 0 deletions website/src/components/docs/DocNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Link from 'next/link';
interface NavItem {
slug: string;
title: string;
category?: string;
}

interface DocNavigationProps {
Expand Down Expand Up @@ -33,6 +34,7 @@ export function DocNavigation({ prev, next }: DocNavigationProps) {
</svg>
{prev.title}
</span>
{prev.category && <span className="text-xs text-zinc-500">{prev.category}</span>}
</Link>
) : (
<div />
Expand All @@ -57,6 +59,7 @@ export function DocNavigation({ prev, next }: DocNavigationProps) {
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</span>
{next.category && <span className="text-xs text-zinc-500">{next.category}</span>}
</Link>
) : (
<div />
Expand Down
56 changes: 40 additions & 16 deletions website/src/components/docs/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,36 +76,60 @@ export function Sidebar() {
</nav>
);

useEffect(() => {
if (!mobileOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setMobileOpen(false);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [mobileOpen]);

return (
<>
{/* Mobile toggle */}
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="fixed bottom-4 right-4 z-50 flex h-12 w-12 items-center justify-center rounded-full bg-accent-indigo text-white shadow-lg lg:hidden"
aria-label="Toggle docs sidebar"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
{mobileOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
) : (
<div className="contents lg:block lg:w-64 lg:shrink-0">
{/* Mobile: inline menu bar under the navbar (no floating button) */}
<div className="sticky top-16 z-30 border-b border-white/5 bg-surface px-6 py-2 backdrop-blur lg:hidden">
<button
type="button"
onClick={() => setMobileOpen(true)}
aria-expanded={mobileOpen}
aria-controls="docs-sidebar"
className="flex items-center gap-2 py-1 text-sm font-medium text-zinc-300 hover:text-white"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</svg>
Docs menu
</button>
</div>

{/* Mobile overlay */}
{mobileOpen && (
<div className="fixed inset-0 z-40 bg-black/60 lg:hidden" onClick={() => setMobileOpen(false)} />
)}

{/* Sidebar */}
{/* Sidebar (sticky column on lg, drawer below) */}
<aside
id="docs-sidebar"
aria-label="Documentation"
className={`fixed top-16 z-40 h-[calc(100vh-4rem)] w-64 overflow-y-auto border-r border-white/5 bg-surface px-2 transition-transform lg:sticky lg:translate-x-0 lg:bg-transparent ${
mobileOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex justify-end pt-2 lg:hidden">
<button
type="button"
onClick={() => setMobileOpen(false)}
className="rounded p-1 text-zinc-400 hover:text-white"
aria-label="Close docs menu"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{sidebarContent}
</aside>
</>
</div>
);
}
41 changes: 35 additions & 6 deletions website/src/components/docs/Toc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { useEffect, useState } from 'react';

interface TocProps {
headings: { id: string; text: string; level: number }[];
/** `rail` is the sticky xl sidebar; `collapsible` is the inline disclosure shown below xl. */
variant?: 'rail' | 'collapsible';
}

export function Toc({ headings }: TocProps) {
export function Toc({ headings, variant = 'rail' }: TocProps) {
const [activeId, setActiveId] = useState<string>('');

useEffect(() => {
Expand Down Expand Up @@ -36,11 +38,7 @@ export function Toc({ headings }: TocProps) {

if (headings.length === 0) return null;

return (
<nav className="sticky top-20 hidden xl:block">
<h4 className="mb-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
On this page
</h4>
const list = (
<ul className="space-y-1.5 text-sm">
{headings.map((heading) => (
<li key={heading.id} className={heading.level === 3 ? 'pl-3' : ''}>
Expand All @@ -62,6 +60,37 @@ export function Toc({ headings }: TocProps) {
</li>
))}
</ul>
);

if (variant === 'collapsible') {
return (
<details className="group mb-6 rounded-lg border border-white/[0.08] bg-white/[0.03] xl:hidden">
<summary className="flex cursor-pointer list-none items-center justify-between px-4 py-3 text-sm font-medium text-zinc-300 [&::-webkit-details-marker]:hidden">
On this page
<svg
className="h-4 w-4 transition-transform group-open:rotate-180"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<nav aria-label="On this page" className="px-4 pb-4">
{list}
</nav>
</details>
);
}

return (
<nav aria-label="On this page" className="sticky top-20 hidden xl:block">
<h4 className="mb-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
On this page
</h4>
{list}
</nav>
);
}
Loading
Loading