diff --git a/web2/src/components/conditional-sidebar.tsx b/web2/src/components/conditional-sidebar.tsx
index 8ea90db..49dfaf0 100644
--- a/web2/src/components/conditional-sidebar.tsx
+++ b/web2/src/components/conditional-sidebar.tsx
@@ -1,10 +1,10 @@
"use client";
import { usePathname } from "next/navigation";
-import { SidebarNav } from "./sidebar-nav";
+import { TableOfContents } from "./table-of-contents";
export function ConditionalSidebar() {
const pathname = usePathname();
if (pathname === "/") return null;
- return
;
+ return
;
}
diff --git a/web2/src/components/get-involved-carousel.tsx b/web2/src/components/get-involved-carousel.tsx
new file mode 100644
index 0000000..e8f0bc8
--- /dev/null
+++ b/web2/src/components/get-involved-carousel.tsx
@@ -0,0 +1,177 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+
+const cards = [
+ {
+ title: "Promote the project",
+ items: [
+ "Share on social media (LinkedIn, X, Reddit)",
+ "Mention Physlib to colleagues, students, or supervisors",
+ "Reference the project in talks, papers, or course materials",
+ "Star the repository on GitHub to boost visibility",
+ ],
+ cta: { label: "View on GitHub", href: "https://github.com/leanprover-community/Physlib", external: true },
+ },
+ {
+ title: "Contribute Lean code",
+ items: [
+ "Pick up open issues labeled 'good first issue' on GitHub",
+ "Formalize theorems, definitions, or calculations from physics",
+ "Golf existing proofs",
+ "Work on the APIs",
+ ],
+ cta: { label: "Browse issues", href: "https://github.com/leanprover-community/Physlib/issues", external: true },
+ },
+ {
+ title: "Create informal results",
+ items: [
+ "Write up clear mathematical statements of physics results",
+ "Provide references to formal proofs",
+ "Describe what a result means physically and why it matters",
+ "Open a GitHub issue or post in Zulip with your informal write-up",
+ ],
+ cta: { label: "Join Zulip", href: "https://leanprover.zulipchat.com/", external: true },
+ },
+ {
+ title: "Review the documentation",
+ items: [
+ "Read through existing docs and note anything unclear or incorrect",
+ "Check that examples and code snippets still work",
+ "Suggest better explanations or missing context",
+ "Report issues or open a PR with fixes directly",
+ ],
+ cta: { label: "Read the docs", href: "/getting-started", external: false },
+ },
+];
+
+export function GetInvolvedCarousel() {
+ const [index, setIndex] = useState(0);
+ const [dir, setDir] = useState<"left" | "right" | null>(null);
+ const [animKey, setAnimKey] = useState(0);
+
+ function go(next: number) {
+ const newIndex = (next + cards.length) % cards.length;
+ setDir(next > index || (index === cards.length - 1 && next === 0) ? "right" : "left");
+ setIndex(newIndex);
+ setAnimKey((k) => k + 1);
+ }
+
+ const card = cards[index];
+
+ return (
+
+ {/* carousel row */}
+
+
+ {/* left arrow */}
+
go(index - 1)}
+ aria-label="Previous"
+ className="flex-shrink-0 size-10 rounded border border-border flex items-center justify-center text-muted hover:text-foreground hover:border-foreground/20 transition-colors"
+ >
+
+
+
+ {/* card */}
+
+
+ {card.title}
+
+
+
+ {card.items.map((item) => (
+
+ —
+ {item}
+
+ ))}
+
+
+ {card.cta.external ? (
+
+ {card.cta.label}
+
+
+ ) : (
+
+ {card.cta.label}
+
+ )}
+
+
+ {/* right arrow */}
+
go(index + 1)}
+ aria-label="Next"
+ className="flex-shrink-0 size-10 rounded border border-border flex items-center justify-center text-muted hover:text-foreground hover:border-foreground/20 transition-colors"
+ >
+
+
+
+
+ {/* dot indicators */}
+
+ {cards.map((_, i) => (
+ go(i)}
+ aria-label={`Go to card ${i + 1}`}
+ className="rounded-full transition-all"
+ style={{
+ width: i === index ? "1.5rem" : "0.375rem",
+ height: "0.375rem",
+ background: i === index ? "var(--accent)" : "var(--border)",
+ }}
+ />
+ ))}
+
+
+ );
+}
+
+function ChevronLeft() {
+ return (
+
+
+
+ );
+}
+
+function ChevronRight() {
+ return (
+
+
+
+ );
+}
+
+function ExternalIcon() {
+ return (
+
+
+
+ );
+}
diff --git a/web2/src/components/navbar.tsx b/web2/src/components/navbar.tsx
index d7b62cc..f73bdbe 100644
--- a/web2/src/components/navbar.tsx
+++ b/web2/src/components/navbar.tsx
@@ -2,17 +2,95 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { useState } from "react";
-import { site, navSections } from "@/lib/site";
+import { useState, useRef } from "react";
+import { site, navSections, type NavSection } from "@/lib/site";
import { ThemeToggle } from "./theme-toggle";
-const links = [
- { label: "Docs", href: site.docs },
- { label: "Todo list", href: "/todo" },
- { label: "Get Involved", href: "/get-involved" },
- { label: "Trackers", href: "/api-tracker" },
- { label: "Sponsor", href: "/sponsor" },
-];
+function NavDropdown({ section, pathname }: { section: NavSection; pathname: string }) {
+ const [open, setOpen] = useState(false);
+ const timerRef = useRef
| null>(null);
+
+ const isActive = section.items.some(
+ (item) =>
+ !item.external &&
+ (item.href === "/" ? pathname === "/" : pathname.startsWith(item.href))
+ );
+
+ function handleMouseEnter() {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ setOpen(true);
+ }
+
+ function handleMouseLeave() {
+ timerRef.current = setTimeout(() => setOpen(false), 100);
+ }
+
+ return (
+
+
+ {section.label}
+
+
+
+
+
+ {open && (
+
+ {section.items.map((item) => {
+ const active =
+ !item.external &&
+ (item.href === "/" ? pathname === "/" : pathname.startsWith(item.href));
+ return item.external ? (
+
+ {item.label}
+
+
+ ) : (
+
+ {active && (
+
+ )}
+ {item.label}
+
+ );
+ })}
+
+ )}
+
+ );
+}
export function Navbar() {
const pathname = usePathname();
@@ -37,24 +115,11 @@ export function Navbar() {
{site.name}
- {/* Desktop nav — centered */}
+ {/* Desktop nav — centered, section dropdowns */}
- {links.map((item) => {
- const active =
- item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
- return (
-
- {item.label}
-
- );
- })}
+ {navSections.map((section) => (
+
+ ))}
{/* Right */}
@@ -192,3 +257,22 @@ function CloseIcon() {
);
}
+
+function ExternalIcon() {
+ return (
+
+
+
+ );
+}
diff --git a/web2/src/components/table-of-contents.tsx b/web2/src/components/table-of-contents.tsx
new file mode 100644
index 0000000..40600f2
--- /dev/null
+++ b/web2/src/components/table-of-contents.tsx
@@ -0,0 +1,108 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { usePathname } from "next/navigation";
+
+interface Heading {
+ id: string;
+ text: string;
+ level: number;
+}
+
+export function TableOfContents() {
+ const pathname = usePathname();
+ const [headings, setHeadings] = useState([]);
+ const [activeId, setActiveId] = useState("");
+ const observerRef = useRef(null);
+
+ // Re-scan headings whenever the page changes
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ // Matches both h2/h3[id] and PageHeader-style
+ const elements = document.querySelectorAll(
+ "main h2[id], main h3[id], main header[id]"
+ );
+ const headingList = Array.from(elements).reduce((acc, el) => {
+ if (el.tagName === "HEADER") {
+ const inner = el.querySelector("h2, h3");
+ if (inner) {
+ acc.push({
+ id: el.id,
+ text: inner.textContent?.trim() ?? "",
+ level: inner.tagName === "H2" ? 2 : 3,
+ });
+ }
+ } else {
+ acc.push({
+ id: el.id,
+ text: el.textContent?.trim() ?? "",
+ level: el.tagName === "H2" ? 2 : 3,
+ });
+ }
+ return acc;
+ }, []);
+ setHeadings(headingList);
+ setActiveId(headingList[0]?.id ?? "");
+ }, 150);
+ return () => clearTimeout(timer);
+ }, [pathname]);
+
+ // Highlight the active section with IntersectionObserver
+ useEffect(() => {
+ if (headings.length === 0) return;
+
+ if (observerRef.current) observerRef.current.disconnect();
+
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ // Pick the topmost visible heading
+ const visible = entries
+ .filter((e) => e.isIntersecting)
+ .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
+ if (visible.length > 0) setActiveId(visible[0].target.id);
+ },
+ { rootMargin: "-72px 0px -60% 0px", threshold: 0 }
+ );
+
+ headings.forEach(({ id }) => {
+ const el = document.getElementById(id);
+ if (el) observerRef.current!.observe(el);
+ });
+
+ return () => observerRef.current?.disconnect();
+ }, [headings]);
+
+ if (headings.length === 0) return null;
+
+ return (
+
+
+
+ On this page
+
+
+
+
+ );
+}
From 1890b53178591bc0ae2e516458440cdcaf21870d Mon Sep 17 00:00:00 2001
From: Alex-Zughaid <117576511+Alex-Zughaid@users.noreply.github.com>
Date: Wed, 12 Aug 2026 17:01:05 +0100
Subject: [PATCH 2/4] added sidebar for other pages
---
web2/src/app/documentation-tracker/page.tsx | 10 +++-
web2/src/app/gh-guide/page.tsx | 58 ++++++---------------
2 files changed, 24 insertions(+), 44 deletions(-)
diff --git a/web2/src/app/documentation-tracker/page.tsx b/web2/src/app/documentation-tracker/page.tsx
index eb70526..ecfd434 100644
--- a/web2/src/app/documentation-tracker/page.tsx
+++ b/web2/src/app/documentation-tracker/page.tsx
@@ -28,7 +28,7 @@ export default function DocumentationTrackerPage() {
-
+
Steps to Help with Documentation
@@ -38,7 +38,9 @@ export default function DocumentationTrackerPage() {
{docSteps.map((step) => (
-
{step.title}
+
+ {step.title}
+
{step.items.map((item, i) => (
@@ -52,6 +54,10 @@ export default function DocumentationTrackerPage() {
);
}
+function slugify(title: string) {
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
+}
+
const docSteps = [
{
title: "Phase 1: Get the Code",
diff --git a/web2/src/app/gh-guide/page.tsx b/web2/src/app/gh-guide/page.tsx
index 048ecb4..fe8645e 100644
--- a/web2/src/app/gh-guide/page.tsx
+++ b/web2/src/app/gh-guide/page.tsx
@@ -1,5 +1,6 @@
import { Card } from "@heroui/react";
import type { Metadata } from "next";
+import { PageHeader } from "@/components/page-header";
import { site } from "@/lib/site";
export const metadata: Metadata = {
@@ -28,12 +29,7 @@ export default function GhGuidePage() {
{/* Step 1 — Deciding on a Problem to Work On */}
-
- 1. Deciding on a Problem to Work On
-
+
@@ -44,7 +40,7 @@ export default function GhGuidePage() {
Before starting any work, you can open an issue on the Physlib repository to
describe the problem you are encountering or the feature you want to
add. This makes maintainers aware of the work you plan on doing and helps with
- tracking the project.
+ tracking the project.
{issueChecklist.map((item) => (
@@ -75,7 +71,7 @@ export default function GhGuidePage() {
The Physlib community is full of members who can point you in the right direction if you want to discuss an idea before opening an issue.
- There is a Physlib channel on the Leanprover Zulip where you can post any questions you have.
+ There is a Physlib channel on the Leanprover Zulip where you can post any questions you have.