diff --git a/e2e/community-page.spec.ts b/e2e/community-page.spec.ts index ea0fc5a..fa1fa65 100644 --- a/e2e/community-page.spec.ts +++ b/e2e/community-page.spec.ts @@ -9,9 +9,13 @@ test.describe('Community page', () => { }); test('renders community stats with compact variant size', async ({ page }) => { - const statBox = page.getByText(community.meeting_frequency, { exact: true }); + const statBox = page + .getByRole('region', { name: 'By the numbers' }) + .getByText(community.meeting_frequency, { exact: true }); await expect(statBox).toBeVisible(); - await expect(statBox).toHaveClass('font-bold mb-1 text-2xl text-foreground'); + await expect(statBox).toHaveClass( + 'font-mono-panels justify-self-end text-[15px] font-medium capitalize' + ); }); }); diff --git a/e2e/pill.spec.ts b/e2e/pill.spec.ts index b7a27d6..9972cbe 100644 --- a/e2e/pill.spec.ts +++ b/e2e/pill.spec.ts @@ -3,8 +3,12 @@ import { expect, test } from '@playwright/test'; test.use({ viewport: { width: 320, height: 844 } }); test('should not overflow viewport', async ({ page }) => { - await page.goto('/'); + await page.goto('/'); - const pill = page.getByText('Community-Driven · Transparent · Impactful'); - await expect(pill).toBeInViewport({ratio: 1}); + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + + const overflow = await page.evaluate( + () => document.documentElement.scrollWidth - document.documentElement.clientWidth + ); + expect(overflow).toBeLessThanOrEqual(0); }); diff --git a/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-darwin.png b/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-darwin.png index 3cc80f6..c6b0574 100644 Binary files a/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-darwin.png and b/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-darwin.png differ diff --git a/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-linux.png b/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-linux.png index 2b7f9c6..189af98 100644 Binary files a/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-linux.png and b/e2e/sign-in-button.spec.ts-snapshots/header-sign-in-chromium-linux.png differ diff --git a/src/app/about/board-of-directors/page.tsx b/src/app/about/board-of-directors/page.tsx index 510fb0e..3126357 100644 --- a/src/app/about/board-of-directors/page.tsx +++ b/src/app/about/board-of-directors/page.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; -import { ButtonLink } from "@/components/ui/button"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { Footer } from "@/components/layout/footer"; +import { BadgeCheck, CircleDollarSign, CircleUserRound, FileText, MessageCircle, ShieldCheck } from "lucide-react"; + +import { OrbitMarks, Panel, PanelActions, PanelButton, PanelEyebrow, PanelSub, RowList } from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; export const metadata: Metadata = { title: "Board of Directors | React Foundation", @@ -62,246 +62,155 @@ const boardMembers: BoardMember[] = [ }, ]; +const philosophyValues = [ + { + title: "Transparent", + description: "All decisions and finances publicly documented", + }, + { + title: "Accountable", + description: "Regular reporting and community oversight", + }, + { + title: "Community-First", + description: "Decisions guided by ecosystem needs", + }, +]; + +const responsibilities = [ + { + icon: ShieldCheck, + title: "Strategic Direction", + description: + "Setting long-term goals, priorities, and initiatives that serve the React ecosystem's growth and sustainability.", + }, + { + icon: CircleDollarSign, + title: "Financial Oversight", + description: + "Ensuring responsible fund management, transparent distribution to maintainers, and regular financial reporting.", + }, + { + icon: MessageCircle, + title: "Community Engagement", + description: + "Maintaining open dialogue with maintainers, contributors, and the broader React community to inform decisions.", + }, + { + icon: FileText, + title: "Governance & Compliance", + description: + "Establishing policies, ensuring legal compliance, and maintaining the foundation's integrity and mission alignment.", + }, +]; + export default function BoardOfDirectorsPage() { return ( -
- {/* Hero Gradient */} -
-
-
- -
-
- {/* Hero Section */} -
- Governance · Leadership · Transparency -
-

- Board of Directors -

-

- Our Board of Directors provides strategic guidance, ensures financial oversight, - and maintains the foundation's commitment to transparency and community-first values. -

+
+ + +
+

+ Board of Directors +

+

+ The Board provides strategic guidance, financial oversight, and a clear commitment + to transparent, community-first governance. +

+
+
+ + + Our governance philosophy +

+ The React Foundation operates with complete transparency and community accountability. + The Board keeps decisions focused on the ecosystem's best interests, with quarterly + reports, open financials, and active community feedback loops. +

+
+ {philosophyValues.map((value) => ( +
+
-
- - {/* Mission Statement */} - -
-

- Our Governance Philosophy -

-

- The React Foundation operates with complete transparency and community accountability. - Our Board ensures that every decision serves the ecosystem's best interests, with - quarterly reports, open financials, and active community feedback loops. -

-
-
-
-
-
-

Transparent

-

- All decisions and finances publicly documented -

-
-
-
-
-
-

Accountable

-

- Regular reporting and community oversight -

-
-
-
-
-
-

Community-First

-

- Decisions guided by ecosystem needs -

-
-
-
-
- - {/* Board Members Grid */} - -
-
-

- Meet the Board -

-

- Leaders committed to building a sustainable future for React -

+ ))} +
+ + + + Meet the board + Leaders committed to building a sustainable future for React. +
+ {boardMembers.map((member) => ( +
+
+
- -
- {boardMembers.map((member) => ( -
- {/* Card Gradient Background */} -
- -
- {/* Headshot Placeholder */} -
-
-
- - - -
-
-
- - {/* Member Info */} -
-

- {member.name} -

-

- {member.title} -

-

- {member.role} -

-
- - {/* Bio */} -

- {member.bio} -

- - {/* Expertise Tags */} -
- {member.expertise.map((skill, skillIndex) => ( - - {skill} - - ))} -
-
-
+

{member.name}

+

{member.title}

+

{member.role}

+

{member.bio}

+
+ {member.expertise.map((skill) => ( + + {skill} + ))}
-
-
- - {/* Responsibilities */} - -
-

- Board Responsibilities -

-
-
-
- - - -
-

- Strategic Direction -

-

- Setting long-term goals, priorities, and initiatives that serve the - React ecosystem's growth and sustainability. -

-
- -
-
- - - -
-

- Financial Oversight -

-

- Ensuring responsible fund management, transparent distribution to maintainers, - and regular financial reporting. -

-
- -
-
- - - -
-

- Community Engagement -

-

- Maintaining open dialogue with maintainers, contributors, and the broader - React community to inform decisions. -

-
- -
-
- - - -
-

- Governance & Compliance -

-

- Establishing policies, ensuring legal compliance, and maintaining the - foundation's integrity and mission alignment. -

-
-
-
-
- - {/* CTA Section */} - -
-

- Learn More About Our Governance -

-

- Explore our transparent operations, quarterly reports, and how we're - building a sustainable future for the React ecosystem. -

-
- - About the Foundation - - - View Our Impact - + + ))} +
+ + + + Board responsibilities + The Board keeps governance, funding, and community accountability aligned. + + {responsibilities.map((responsibility) => ( +
+
-
-
-
- -
+
+ ))} + + + + +
+
+ Learn more about our governance +

+ Explore transparent operations, quarterly reports, and the work behind a sustainable + React ecosystem. +

+
+ + + About the Foundation + + + View Our Impact + + +
+
+ +
); } diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 11497b1..9bff0b4 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,13 +1,23 @@ import type { Metadata } from "next"; -import Link from "next/link"; -import { ButtonLink } from "@/components/ui/button"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { Footer } from "@/components/layout/footer"; +import { Code, Landmark } from "lucide-react"; + import { BecomeContributor } from "@/components/home/become-contributor"; import { EcosystemLibraries } from "@/components/home/ecosystem-libraries"; -import { FoundingMembers } from "@/components/home/founding-members"; import { ExecutiveMessage } from "@/components/home/executive-message"; +import { FoundingMembers } from "@/components/home/founding-members"; +import { + OrbitMarks, + Panel, + PanelActions, + PanelButton, + PanelEyebrow, + PanelSub, + Row, + RowArrow, + RowList, + RowRight, +} from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; import { RFDS } from "@/components/rfds"; const sections = [ @@ -25,309 +35,206 @@ export const metadata: Metadata = { description: "Learn about the React Foundation's mission, governance, and how we support the ecosystem.", }; +const MISSION_POINTS = [ + { + title: "Sustainable Funding", + description: "Creating reliable revenue streams that support open source maintainers", + }, + { + title: "Full Transparency", + description: "Quarterly reports showing exactly how funds are distributed", + }, + { + title: "Community First", + description: "Decisions driven by community needs and maintainer feedback", + }, +]; + +const HOW_IT_WORKS_STEPS = [ + { + title: "Contribute to the Ecosystem", + description: + "Submit code, documentation, RFCs, and bug reports to React and 54+ ecosystem libraries. Your contributions directly improve the tools millions of developers use every day.", + }, + { + title: "Join the Community", + description: + "Organize meetups, create educational content, teach workshops, or help other developers learn React. Community organizers and educators are essential to ecosystem growth.", + }, + { + title: "Support Through the Store", + description: + "One way to fund the ecosystem is through our official merchandise store. 100% of profits support maintainers, educators, and community organizers based on transparent impact metrics.", + }, + { + title: "Transparent Impact", + description: + "Quarterly impact reports detail exactly how funds support maintainers, education, and accessibility initiatives. Full transparency in how contributions make a difference.", + }, +]; + +const GOVERNANCE_BODIES = [ + { + icon: Landmark, + href: "/about/board-of-directors", + title: "Board of Directors", + subtitle: "Strategic Leadership · Financial Oversight · Governance", + description: + "Our Board provides strategic guidance, ensures financial oversight, and maintains the foundation's commitment to transparency and community-first values.", + }, + { + icon: Code, + href: "/about/technical-steering-committee", + title: "Technical Steering Committee", + subtitle: "Technical Excellence · Innovation · Open Standards", + description: + "The TSC drives technical excellence across the React ecosystem, establishing standards, best practices, and supporting innovation in libraries and tools.", + }, +]; + +const GOVERNANCE_PRINCIPLES = [ + { title: "Open Financials", description: "Every dollar tracked and reported publicly" }, + { title: "Community Input", description: "Major decisions informed by maintainer feedback" }, + { title: "Quarterly Reports", description: "Detailed impact metrics published every quarter" }, + { title: "Open Source Values", description: "Built on the same principles as the ecosystem we support" }, +]; + export default function AboutPage() { return ( -
-
-
-
- -
-
-
- {/* Hero */} -
- Our Story · Our Mission · Our Values -
-

+
+
+
+ + +
+

About React Foundation

-

+

We're building a sustainable future for the React ecosystem through - community funding, transparent governance, and unwavering support for - the maintainers who make it all possible. + community funding, transparent governance, and support for the + maintainers who make it all possible.

-

+ - {/* Executive Message */} -
+
- {/* Mission */} -
- -
-

- Our Mission -

-

- The React Foundation exists to ensure the React ecosystem thrives for - generations to come. We provide direct financial support to maintainers, - fund educational initiatives, and ensure accessibility for developers - worldwide. -

-
-
-
-
-
-
-

Sustainable Funding

-

- Creating reliable revenue streams that support open source maintainers -

-
+ + Our mission +

+ The React Foundation exists to ensure the React ecosystem{" "} + thrives for generations to come. We + provide direct financial support to maintainers, fund educational + initiatives, and ensure accessibility for developers worldwide. +

+ + {MISSION_POINTS.map((point) => ( +
+

{point.title}

+

{point.description}

-
-
-
-
-
-

Full Transparency

-

- Quarterly reports showing exactly how funds are distributed -

-
-
-
-
-
-
-
-

Community First

-

- Decisions driven by community needs and maintainer feedback + ))} + + + + + How it works +

+ {HOW_IT_WORKS_STEPS.map((step, index) => ( +
+ + {index + 1} + +
+

{step.title}

+

+ {step.description}

-
-
-
-
- - {/* How It Works */} -
- -
-

- How It Works -

-
-
-
- 1 -
-

- Contribute to the Ecosystem -

-

- Submit code, documentation, RFCs, and bug reports to React and 54+ - ecosystem libraries. Your contributions directly improve the tools - millions of developers use every day. -

-
- -
-
- 2 -
-

- Join the Community -

-

- Organize meetups, create educational content, teach workshops, or - help other developers learn React. Community organizers and educators - are essential to ecosystem growth. -

-
- -
-
- 3 -
-

- Support Through the Store -

-

- One way to fund the ecosystem is through our official merchandise store. - 100% of profits support maintainers, educators, and community organizers - based on transparent impact metrics. -

-
- -
-
- 4 -
-

- Transparent Impact -

-

- Quarterly impact reports detail exactly how funds support maintainers, - education, and accessibility initiatives. Full transparency in how - contributions make a difference. -

-
-
-
-
-
+ ))} +
+ - {/* Founding Members */} -
+
- {/* Ecosystem Libraries */} -
- +
+
- {/* Governance / Communities */} -
- -
-

- Transparent Governance -

-

- The React Foundation operates with complete transparency. All funding - decisions, impact reports, and financial details are published quarterly - for community review and feedback. -

- - {/* Governance Bodies */} -
- -
-
- - - -
-
-

- Board of Directors -

-

- Strategic Leadership · Financial Oversight · Governance -

-
- - - + + Transparent governance + + The React Foundation operates with complete transparency. All funding + decisions, impact reports, and financial details are published quarterly + for community review and feedback. + + + {GOVERNANCE_BODIES.map((body) => ( + + + ))} + +
+ {GOVERNANCE_PRINCIPLES.map((principle) => ( +
-
-
- - - -
-
-

- Technical Steering Committee -

-

- Technical Excellence · Innovation · Open Standards -

-
- - - -
-

- The TSC drives technical excellence across the React ecosystem, establishing - standards, best practices, and supporting innovation in libraries and tools. -

- -
- - {/* Governance Principles */} -
-
-

Open Financials

-

- Every dollar tracked and reported publicly -

+

{principle.title}

+

{principle.description}

-
-

Community Input

-

- Major decisions informed by maintainer feedback -

-
-
-

Quarterly Reports

-

- Detailed impact metrics published every quarter -

-
-
-

Open Source Values

-

- Built on the same principles as the ecosystem we support -

-
-
-
-
-
+ ))} +
+ - {/* Become a Contributor */} -
- +
-
- {/* CTA */} - -
-

- Ready to Make an Impact? -

-

- Start supporting the React ecosystem today. Every contribution helps build - a sustainable future for open source. -

-
- - Shop the Store - - - View Our Impact - -
-
-
+ + Ready to make an impact? +

+ Start supporting the React ecosystem today. Every contribution helps build + a sustainable future for open source. +

+ + + Shop the Store + + + View Our Impact + + +
- {/* Table of Contents Sidebar */} - -
+
-
+
); } diff --git a/src/app/about/technical-steering-committee/page.tsx b/src/app/about/technical-steering-committee/page.tsx index 595fd35..d0cfb24 100644 --- a/src/app/about/technical-steering-committee/page.tsx +++ b/src/app/about/technical-steering-committee/page.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; -import { ButtonLink } from "@/components/ui/button"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { Footer } from "@/components/layout/footer"; +import { BadgeCheck, BookOpen, CircleUserRound, Code2, Globe2, Lightbulb, ShieldCheck, Wrench, Zap } from "lucide-react"; + +import { OrbitMarks, Panel, PanelActions, PanelButton, PanelEyebrow, PanelSub, RowList } from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; export const metadata: Metadata = { title: "Technical Steering Committee | React Foundation", @@ -76,328 +76,211 @@ const committeeMembers: CommitteeMember[] = [ }, ]; +const missionValues = [ + { + title: "Excellence", + description: "Maintaining high technical standards across all projects", + }, + { + title: "Innovation", + description: "Supporting experimentation and emerging technologies", + }, + { + title: "Collaboration", + description: "Fostering cooperation between ecosystem projects", + }, +]; + +const responsibilities = [ + { + icon: Code2, + title: "Technical Standards", + description: + "Establishing and maintaining technical standards, API design guidelines, and best practices for ecosystem libraries.", + }, + { + icon: Lightbulb, + title: "Innovation Support", + description: + "Evaluating emerging technologies, experimental features, and new patterns that could benefit the React ecosystem.", + }, + { + icon: BookOpen, + title: "Documentation & Education", + description: + "Ensuring comprehensive technical documentation, guides, and educational resources for the community.", + }, + { + icon: Globe2, + title: "Ecosystem Coordination", + description: + "Facilitating collaboration between projects, resolving technical conflicts, and promoting interoperability.", + }, + { + icon: ShieldCheck, + title: "Security & Quality", + description: + "Establishing security protocols, vulnerability response processes, and quality assurance standards.", + }, + { + icon: Zap, + title: "Performance & Optimization", + description: + "Guiding performance optimization strategies, benchmarking practices, and establishing performance budgets.", + }, +]; + +const workingGroups = [ + { + title: "Framework Interop", + description: "Ensuring libraries work seamlessly across React frameworks", + }, + { + title: "Testing Standards", + description: "Developing unified testing approaches and tools", + }, + { + title: "Server Components", + description: "Exploring patterns for RSC adoption in libraries", + }, + { + title: "Type Safety", + description: "Improving TypeScript integration across ecosystem", + }, + { + title: "Accessibility", + description: "Establishing a11y standards and best practices", + }, + { + title: "Build Tooling", + description: "Optimizing build systems and developer experience", + }, +]; + export default function TechnicalSteeringCommitteePage() { return ( -
- {/* Hero Gradient */} -
-
-
+
+ + +
+

+ Technical Steering Committee +

+

+ The TSC drives technical excellence across React libraries, tools, and frameworks + through standards, best practices, and support for responsible innovation. +

+
+
-
-
- {/* Hero Section */} -
- Technical Excellence · Innovation · Open Standards -
-

- Technical Steering Committee -

-

- Our Technical Steering Committee (TSC) drives technical excellence across the - React ecosystem, establishing standards, best practices, and supporting innovation - in libraries, tools, and frameworks. -

+ + Our technical mission +

+ The TSC keeps technical decisions aligned with the ecosystem's long-term health: + maintainer guidance, interoperability standards, and innovation that preserves + stability and developer trust. +

+
+ {missionValues.map((value) => ( +
+
-
+ ))} +
+ - {/* Mission Statement */} - -
-

- Our Technical Mission -

-

- The TSC ensures technical decisions align with the ecosystem's long-term health, - supporting maintainers with guidance, establishing interoperability standards, and - fostering innovation while maintaining stability and developer trust. -

-
-
-
-
-
-

Excellence

-

- Maintaining high technical standards across all projects -

-
-
-
-
-
-

Innovation

-

- Supporting experimentation and emerging technologies -

-
-
-
-
-
-

Collaboration

-

- Fostering cooperation between ecosystem projects -

-
+ + Meet the committee + Technical experts dedicated to React ecosystem excellence. +
+ {committeeMembers.map((member) => ( +
+
+
-
-
- - {/* Committee Members Grid */} - -
-
-

- Meet the Committee -

-

- Technical experts dedicated to React ecosystem excellence -

-
- -
- {committeeMembers.map((member) => ( -
- {/* Card Gradient Background */} -
- -
- {/* Headshot Placeholder */} -
-
-
- - - -
-
-
- - {/* Member Info */} -
-

- {member.name} -

-

- {member.title} -

-

- {member.role} -

-
- - {/* Bio */} -

- {member.bio} -

- - {/* Expertise Tags */} -
- {member.expertise.map((skill, skillIndex) => ( - - {skill} - - ))} -
-
-
+

{member.name}

+

{member.title}

+

{member.role}

+

{member.bio}

+
+ {member.expertise.map((skill) => ( + + {skill} + ))}
-
-
- - {/* Responsibilities */} - -
-

- Committee Responsibilities -

-
-
-
- - - -
-

- Technical Standards -

-

- Establishing and maintaining technical standards, API design guidelines, - and best practices for ecosystem libraries. -

-
- -
-
- - - -
-

- Innovation Support -

-

- Evaluating emerging technologies, experimental features, and new patterns - that could benefit the React ecosystem. -

-
- -
-
- - - -
-

- Documentation & Education -

-

- Ensuring comprehensive technical documentation, guides, and educational - resources for the community. -

-
+ + ))} +
+ -
-
- - - -
-

- Ecosystem Coordination -

-

- Facilitating collaboration between projects, resolving technical conflicts, - and promoting interoperability. -

-
- -
-
- - - -
-

- Security & Quality -

-

- Establishing security protocols, vulnerability response processes, and - quality assurance standards. -

-
- -
-
- - - -
-

- Performance & Optimization -

-

- Guiding performance optimization strategies, benchmarking practices, and - establishing performance budgets. -

-
+ + Committee responsibilities + The committee supports standards, security, coordination, and performance across the ecosystem. + + {responsibilities.map((responsibility) => ( +
+
-
+
+ ))} + + - {/* Working Groups */} - -
-

- Technical Working Groups -

-

- The TSC organizes focused working groups to tackle specific technical challenges, - explore new patterns, and develop proposals for ecosystem-wide adoption. -

-
-
-

Framework Interop

-

- Ensuring libraries work seamlessly across React frameworks -

-
-
-

Testing Standards

-

- Developing unified testing approaches and tools -

-
-
-

Server Components

-

- Exploring patterns for RSC adoption in libraries -

-
-
-

Type Safety

-

- Improving TypeScript integration across ecosystem -

-
-
-

Accessibility

-

- Establishing a11y standards and best practices -

-
-
-

Build Tooling

-

- Optimizing build systems and developer experience -

-
+ + Technical working groups + + The TSC organizes focused working groups to tackle technical challenges and develop ecosystem-wide proposals. + + + {workingGroups.map((group) => ( +
+
-
+
+ ))} + + - {/* CTA Section */} - -
-

- Get Involved in Technical Discussions -

-

- Join our technical discussions, propose new standards, and help shape the - future of the React ecosystem. -

-
- - About the Foundation - - - View Our Impact - -
-
-
- -
+ +
+
+ Get involved in technical discussions +

+ Join technical discussions, propose standards, and help shape the future of the React ecosystem. +

+
+ + + About the Foundation + + + View Our Impact + + +
+
-
); } diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index b3328e8..592ec4a 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -5,94 +5,102 @@ import { useSearchParams } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; -export default function SignInPage() { - const searchParams = useSearchParams(); - const callbackUrl = searchParams?.get("callbackUrl") || "/"; +import { Panel } from "@/components/panels/panel"; +import { cn } from "@/lib/cn"; + +const FOCUS_RING = cn( + "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[#16181D]", +); - return ( -
- {/* Background gradient */} -
-
-
+export default function SignInPage() { + const searchParams = useSearchParams(); + const callbackUrl = searchParams?.get("callbackUrl") || "/"; -
- {/* Logo and Title */} -
-
- React Foundation -
-

Welcome Back

-

- Sign in to access your React Foundation account -

-
+ return ( +
+
+ +
+
+ React Foundation +
+

+ Welcome Back +

+

+ Sign in to access your React Foundation account +

+
- {/* Sign In Options */} -
- {/* GitHub */} - +
+ - {/* GitLab */} - + -

- We verify your contributions to the React ecosystem -

-
+

+ We verify your contributions to the React ecosystem +

+
+
- {/* Back Link */} -
- - ← Back to home - -
-
-
- ); +
+ + ← Back to home + +
+
+
+ ); } diff --git a/src/app/authors/[slug]/page.tsx b/src/app/authors/[slug]/page.tsx index ba356fc..afd182f 100644 --- a/src/app/authors/[slug]/page.tsx +++ b/src/app/authors/[slug]/page.tsx @@ -1,182 +1,179 @@ import type { Metadata } from "next"; -import { notFound } from "next/navigation"; import Image from "next/image"; -import Link from "next/link"; -import { Pill } from "@/components/ui/pill"; -import { Footer } from "@/components/layout/footer"; -import { getAuthorBySlug, getAllAuthors } from "@/lib/authors"; +import { notFound } from "next/navigation"; + +import { Panel, PanelEyebrow, PanelPlainLink, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; +import { getAllAuthors, getAuthorBySlug, type Author } from "@/lib/authors"; import { getAllUpdates } from "@/lib/updates"; type AuthorPageProps = { - params: Promise<{ - slug: string; - }>; + params: Promise<{ + slug: string; + }>; }; export async function generateStaticParams() { - const authors = getAllAuthors(); - return authors.map((author) => ({ - slug: author.slug, - })); + const authors = getAllAuthors(); + + return authors.map((author) => ({ + slug: author.slug, + })); } export async function generateMetadata({ - params, + params, }: AuthorPageProps): Promise { - const { slug } = await params; - const author = getAuthorBySlug(slug); - - if (!author) { - return { - title: "Author not found", - }; - } - - return { - title: author.name, - description: `${author.title} - ${author.bio}`, - }; + const { slug } = await params; + const author = getAuthorBySlug(slug); + + if (!author) { + return { + title: "Author not found", + }; + } + + return { + title: author.name, + description: `${author.title} - ${author.bio}`, + }; +} + +const SOCIAL_LINKS: Array<{ + key: "github" | "twitter" | "linkedin" | "website"; + label: string; +}> = [ + { key: "github", label: "GitHub" }, + { key: "twitter", label: "Twitter" }, + { key: "linkedin", label: "LinkedIn" }, + { key: "website", label: "Website" }, +]; + +function formatDate(date: string) { + return new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +function AuthorSocialLinks({ author }: { author: Author }) { + const hasSocialLinks = SOCIAL_LINKS.some((link) => author[link.key]); + + if (!hasSocialLinks) { + return null; + } + + return ( +
+ {SOCIAL_LINKS.map((link) => { + const href = author[link.key]; + + if (!href) { + return null; + } + + return ( + + {link.label} → + + ); + })} +
+ ); } export default async function AuthorPage({ params }: AuthorPageProps) { - const { slug } = await params; - const author = getAuthorBySlug(slug); - - if (!author) { - notFound(); - } - - // Get updates by this author - const authorUpdates = getAllUpdates().filter( - (update) => update.metadata.author === slug - ); - - return ( -
-
-
-
- -
-
- {/* Author Profile */} -
-
- {author.avatar && ( -
- {author.name} -
- )} -
-

- {author.name} -

-

{author.title}

-
-

- {author.bio} -

- - {/* Social Links */} -
- {author.github && ( - - GitHub → - - )} - {author.twitter && ( - - Twitter → - - )} - {author.linkedin && ( - - LinkedIn → - - )} - {author.website && ( - - Website → - - )} -
-
-
- - {/* Author's Updates */} - {authorUpdates.length > 0 && ( -
-

- Updates by {author.name} -

-
- {authorUpdates.map((update) => ( - - -

- {update.metadata.title} -

-

- {update.metadata.description} -

- - ))} -
-
- )} - - {/* Back Link */} -
- - ← All authors - -
-
-
- -
-
- ); + const { slug } = await params; + const author = getAuthorBySlug(slug); + + if (!author) { + notFound(); + } + + const authorUpdates = getAllUpdates().filter((update) => update.metadata.author === slug); + + return ( +
+
+ +
+ {author.avatar && ( +
+ {author.name} +
+ )} +
+

+ {author.name} +

+

{author.title}

+

+ {author.bio} +

+
+ +
+
+
+
+ + {authorUpdates.length > 0 && ( + + Updates by {author.name} + + {authorUpdates.map((update) => ( + + +
+

+ {update.metadata.title} +

+

+ {update.metadata.description} +

+
+ + Read update + +
+ ))} +
+
+ )} + + +

+ Author navigation +

+ All authors +
+
+ +
+ ); } diff --git a/src/app/authors/page.tsx b/src/app/authors/page.tsx index 704707e..1b02e2c 100644 --- a/src/app/authors/page.tsx +++ b/src/app/authors/page.tsx @@ -1,102 +1,104 @@ import type { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { getAllAuthors } from "@/lib/authors"; -import { Footer } from "@/components/layout/footer"; + +import { Panel, PanelEyebrow, PanelSub } from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; +import { getAllAuthors, type Author } from "@/lib/authors"; export const metadata: Metadata = { - title: "Authors", - description: "Meet the people behind the React Foundation.", + title: "Authors", + description: "Meet the people behind the React Foundation.", }; -export default function AuthorsPage() { - const authors = getAllAuthors(); +const SOCIAL_LINKS: Array<{ + key: "github" | "twitter" | "linkedin" | "website"; + label: string; +}> = [ + { key: "github", label: "GitHub" }, + { key: "twitter", label: "Twitter" }, + { key: "linkedin", label: "LinkedIn" }, + { key: "website", label: "Website" }, +]; + +function AuthorSocialLinks({ author }: { author: Author }) { + const hasSocialLinks = SOCIAL_LINKS.some((link) => author[link.key]); + + if (!hasSocialLinks) { + return null; + } - return ( -
-
-
-
+ return ( +
+ {SOCIAL_LINKS.map((link) => { + const href = author[link.key]; -
-
- {/* Hero */} -
- Meet the Team · Contributors · Leadership -
-

- Authors -

-

- Meet the people building and leading the React Foundation. -

-
-
+ if (!href) { + return null; + } - {/* Authors Grid */} - -
- {authors.map((author, idx) => ( - - - {author.avatar && ( -
-
- {author.name} -
-
- )} -

- {author.name} -

-

{author.title}

-

{author.bio}

+ return ( + + {link.label} + + ); + })} +
+ ); +} + +export default function AuthorsPage() { + const authors = getAllAuthors(); - {/* Social Links */} -
- {author.github && ( - e.stopPropagation()} - > - GitHub - - )} - {author.twitter && ( - e.stopPropagation()} - > - Twitter - - )} -
- - - ))} - - - -
+ return ( +
+
+ +

+ Authors +

+ Meet the people building and leading the React Foundation. +
-
-
- ); + + People +
+ {authors.map((author) => ( +
+ + {author.avatar && ( +
+ {author.name} +
+ )} +

+ {author.name} +

+

{author.title}

+

{author.bio}

+ + +
+ ))} +
+
+ + +
+ ); } diff --git a/src/app/become-a-member/page.tsx b/src/app/become-a-member/page.tsx index 433f3f1..e1b60e9 100644 --- a/src/app/become-a-member/page.tsx +++ b/src/app/become-a-member/page.tsx @@ -1,213 +1,190 @@ import type { Metadata } from "next"; -import Link from "next/link"; -import { ButtonLink } from "@/components/ui/button"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { Footer } from "@/components/layout/footer"; + +import { + Panel, + PanelActions, + PanelButton, + PanelEyebrow, + PanelPlainLink, + PanelSub, + RowList, +} from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; const ENROLLMENT_URL = - "https://enrollment.lfx.linuxfoundation.org/?project=react-foundation"; + "https://enrollment.lfx.linuxfoundation.org/?project=react-foundation"; const memberReasons = [ - { - title: "Sustain core infrastructure", - description: - "Help fund the maintainers, tooling, and shared services that millions of developers and teams rely on every day.", - }, - { - title: "Invest in ecosystem health", - description: - "Support programs that strengthen libraries, education, security, accessibility, and long-term project resilience.", - }, - { - title: "Shape responsible growth", - description: - "Join a member community aligned around transparent governance and a healthy future for React across companies and communities.", - }, + { + title: "Sustain core infrastructure", + description: + "Help fund the maintainers, tooling, and shared services that millions of developers and teams rely on every day.", + }, + { + title: "Invest in ecosystem health", + description: + "Support programs that strengthen libraries, education, security, accessibility, and long-term project resilience.", + }, + { + title: "Shape responsible growth", + description: + "Join a member community aligned around transparent governance and a healthy future for React across companies and communities.", + }, ]; const membershipBenefits = [ - "Visible support for React's independent ecosystem stewardship", - "Participation in foundation member conversations and priorities", - "Opportunities to collaborate on ecosystem sustainability programs", - "Connection with maintainers, educators, tool authors, and platform teams", + "Visible support for React's independent ecosystem stewardship", + "Participation in foundation member conversations and priorities", + "Opportunities to collaborate on ecosystem sustainability programs", + "Connection with maintainers, educators, tool authors, and platform teams", ]; const investmentAreas = [ - "Maintainer support", - "Community education", - "Ecosystem tooling", - "Governance operations", - "Accessibility and inclusion", - "Long-term project resilience", + "Maintainer support", + "Community education", + "Ecosystem tooling", + "Governance operations", + "Accessibility and inclusion", + "Long-term project resilience", ]; export const metadata: Metadata = { - title: "Become a Member | React Foundation", - description: - "Become a React Foundation member and help sustain the ecosystem, maintainers, and community programs that support React's future.", + title: "Become a Member | React Foundation", + description: + "Become a React Foundation member and help sustain the ecosystem, maintainers, and community programs that support React's future.", }; export default function BecomeMemberPage() { - return ( -
-
-
-
- -
-
-
-
-
-

- Help sustain the future of React. -

-

- React is more than a library. It is an ecosystem of maintainers, - educators, tools, frameworks, and community spaces that help teams - build for the web and beyond. Members help keep that - ecosystem healthy, independent, and durable. -

-
-
- - Join as a member - - - See what membership supports - -
-
- - -
- - -
-
-

- Members fund the shared work behind React. -

-

- Your support helps the foundation invest where individual projects - and volunteer maintainers should not have to carry the burden alone. -

-
- -
- {memberReasons.map((reason) => ( -
-

- {reason.title} -

-

- {reason.description} -

-
- ))} -
-
-
- - -
-
- Member Value -

- Built for organizations that depend on React. -

-

- Membership is for companies, platforms, agencies, and teams who - want React to remain a strong open ecosystem with clear stewardship - and practical support for the people doing the work. -

-
- -
- {membershipBenefits.map((benefit) => ( -
- {benefit} -
- ))} -
-
-
- - -
-
-

- Practical support for a broad ecosystem. -

-

- React Foundation membership helps create capacity for work that - benefits the whole ecosystem, not just one product roadmap or one - organization's priorities. -

-
- -
- {investmentAreas.map((area) => ( -
- {area} -
- ))} -
-
-
- - -
- Start Membership -

- Join the organizations helping React stay healthy for everyone. -

-

- Begin with the Linux Foundation enrollment form, or return to the - foundation site to learn more about our mission and governance. -

-
- - Join now - - - Learn about the foundation - -
-
-
-
-
- -
-
- ); + return ( +
+ +
+
+

+ Help sustain the future of React. +

+

+ React is more than a library. It is an ecosystem of maintainers, + educators, tools, frameworks, and community spaces that help teams + build for the web and beyond. Members help keep that ecosystem + healthy, independent, and durable. +

+ + + Join as a member + + + See what membership supports + + +
+ + +
+
+ + + Membership +

+ Members fund the shared work behind React. +

+ + Your support helps the foundation invest where individual projects + and volunteer maintainers should not have to carry the burden alone. + + + {memberReasons.map((reason) => ( +
+

{reason.title}

+

+ {reason.description} +

+
+ ))} +
+
+ + +
+
+ Member value +

+ Built for organizations that depend on React. +

+ + Membership is for companies, platforms, agencies, and teams who + want React to remain a strong open ecosystem with clear stewardship + and practical support for the people doing the work. + +
+ +
+ {membershipBenefits.map((benefit) => ( +
+ {benefit} +
+ ))} +
+
+
+ + + Investment areas +

+ Practical support for a broad ecosystem. +

+ + React Foundation membership helps create capacity for work that + benefits the whole ecosystem, not just one product roadmap or one + organization's priorities. + + + {investmentAreas.map((area) => ( +
+ {area} +
+ ))} +
+
+ + +
+ Start membership +

+ Join the organizations helping React stay healthy for everyone. +

+

+ Begin with the Linux Foundation enrollment form, or return to the + foundation site to learn more about our mission and governance. +

+
+ + Join now + + Learn about the foundation +
+
+
+ + +
+ ); } diff --git a/src/app/communities/[slug]/page.tsx b/src/app/communities/[slug]/page.tsx index db0a3dc..6d1bcba 100644 --- a/src/app/communities/[slug]/page.tsx +++ b/src/app/communities/[slug]/page.tsx @@ -5,15 +5,25 @@ import { notFound } from 'next/navigation'; import Link from 'next/link'; +import { Cake, Calendar, CircleUserRound, MapPin, Users } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; import { getCommunityHostLabel } from '@/lib/community-host'; import { getCommunityBySlug } from '@/lib/redis-communities'; -import { RFDS } from '@/components/rfds'; +import { OrbitMarks, Panel, PanelActions, PanelButton, PanelEyebrow, RowList } from '@/components/panels/panel'; +import { PanelsFooter } from '@/components/panels/panels-footer'; import type { Metadata } from 'next'; interface CommunityPageProps { params: Promise<{ slug: string }>; } +const FOCUS_RING = + 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[#16181D]'; + +const INK_BUTTON = `panels-anim inline-flex items-center justify-center rounded-xl border border-[#16181D] bg-[#16181D] px-6 py-3.5 text-[15px] font-semibold leading-[1.2] text-[#F6F7F9]! hover:border-[#07090D] hover:bg-[#07090D] ${FOCUS_RING}`; + +const OUTLINE_BUTTON = `panels-anim inline-flex items-center justify-center rounded-xl border border-[#16181D] bg-transparent px-6 py-3.5 text-[15px] font-semibold leading-[1.2] text-[#16181D] hover:bg-[rgba(22,24,29,0.08)] ${FOCUS_RING}`; + export async function generateMetadata({ params, }: CommunityPageProps): Promise { @@ -43,363 +53,338 @@ export default async function CommunityPage({ params }: CommunityPageProps) { const tierInfo = getTierInfo(community.cois_tier); return ( -
- {/* Hero Section */} -
-
-
- {/* Breadcrumb */} - - - {/* Header */} -
-
-
-

- {community.name} -

- {community.verified && ( - - ✓ - - )} -
+
+ + +
+ -

- {community.city} - {community.region && `, ${community.region}`}, {community.country} -

+
+
+
+

+ {community.name} +

+ {community.verified && ( + + ✓ Verified + + )} +
- {/* Event Types */} -
- {community.event_types.map((type) => ( - - {type} - - ))} -
+

+ {community.city} + {community.region && `, ${community.region}`}, {community.country} +

+ +
+ {community.event_types.map((type) => ( + + {type} + + ))}
+
- {/* CoIS Tier Badge */} - {tierInfo && ( -
-
{tierInfo.icon}
-
- {tierInfo.label} -
-
- CoIS Tier + {tierInfo && ( +
+

+ CoIS Tier +

+
+ {tierInfo.label} +
+ {community.cois_score && ( +
+ {community.cois_score.toFixed(2)}
- {community.cois_score && ( -
- {community.cois_score.toFixed(2)} + )} +
+ )} +
+ + + {community.meetup_url && ( + + Join on {getCommunityHostLabel(community.meetup_url)} → + + )} + {community.website && ( + + Visit Website → + + )} + {community.discord_url && ( + + Join Discord + + )} + +
+ + +
+
+ + About this community +

+ {community.description} +

+
+ + + By the numbers + + + + + + + + + {community.organizers && community.organizers.length > 0 && ( + + Organizers + + {community.organizers.map((organizer) => ( +
+
+
- )} -
- )} -
+
+
+ {organizer.name} +
+
+ {organizer.role} +
+ {organizer.twitter_handle && ( + + @{organizer.twitter_handle} + + )} +
+
+ ))} + + + )} - {/* Quick Actions */} - + + + {community.last_event_date && ( + + Latest activity +

+ Last event:{' '} + + {new Date(community.last_event_date).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + +

+
+ )}
-
- - {/* Main Content */} -
-
-
- {/* Main Column */} -
- {/* About */} -
-

- About This Community -

-

- {community.description} -

-
- {/* Stats Grid */} -
- - - +

+ Community details +

+ +
+

Status

+
+ +
+
+ +
+

+ Quick Info +

+ + + {community.coordinates && ( + - -
- - {/* Organizers */} - {community.organizers && community.organizers.length > 0 && ( -
-

- Organizers -

-
- {community.organizers.map((organizer) => ( -
-
- 👤 -
-
-
- {organizer.name} -
-
- {organizer.role} -
- {organizer.twitter_handle && ( - - @{organizer.twitter_handle} - - )} -
-
- ))} -
-
)} + +
- {/* Languages */} -
-

- Languages -

-
- - {community.primary_language} - - {community.secondary_languages?.map((lang) => ( - - {lang} - - ))} -
-
+ {(community.twitter_handle || + community.linkedin_url || + community.slack_url) && ( +
+

+ Connect +

+ + {community.twitter_handle && ( + + Twitter → + + )} + {community.linkedin_url && ( + + LinkedIn → + + )} + {community.slack_url && ( + + Slack → + + )} + +
+ )} - {/* Last Event */} - {community.last_event_date && ( -
-

- Latest Activity -

-

- Last event:{' '} - - {new Date(community.last_event_date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - })} - -

-
- )} +
+

+ Want to Organize? +

+

+ Start your own React community and earn CoIS rewards +

+
+ + Learn More +
+
+ +
- {/* Sidebar */} - -
-
- +
); } -function StatBox({ +function StatRow({ + icon: Icon, label, value, - icon, capitalize, }: { + icon: LucideIcon; label: string; value: string; - icon: string; capitalize?: boolean; }) { return ( - +
+
); } function StatusBadge({ status }: { status: string }) { const config = { - active: { bg: 'bg-green-500/10', text: 'text-green-600 dark:text-green-400', label: 'Active' }, - new: { bg: 'bg-blue-500/10', text: 'text-blue-600 dark:text-blue-400', label: 'New' }, - paused: { bg: 'bg-yellow-500/10', text: 'text-yellow-600 dark:text-yellow-400', label: 'Paused' }, - inactive: { bg: 'bg-gray-500/10', text: 'text-gray-600 dark:text-gray-400', label: 'Inactive' }, - }[status] || { bg: 'bg-muted', text: 'text-muted-foreground', label: status }; + active: { bg: 'bg-[#44AC99]', text: 'text-[#16181D]', label: 'Active' }, + new: { bg: 'bg-[#087EA4]', text: 'text-white', label: 'New' }, + paused: { bg: 'bg-[#C76A15]', text: 'text-[#16181D]', label: 'Paused' }, + inactive: { bg: 'bg-[#5E687E]', text: 'text-white', label: 'Inactive' }, + }[status] || { bg: 'border border-[rgba(22,24,29,0.2)]', text: 'text-[#5E687E]', label: status }; return ( -
+
{config.label}
); @@ -407,9 +392,9 @@ function StatusBadge({ status }: { status: string }) { function InfoRow({ label, value }: { label: string; value: string }) { return ( -
- {label}: - {value} +
+ {label} + {value}
); } @@ -418,30 +403,22 @@ function getTierInfo(tier?: string) { switch (tier) { case 'platinum': return { - icon: '💎', label: 'Platinum', - textColor: 'text-cyan-400', description: 'Top 5% - Elite community builder', }; case 'gold': return { - icon: '🏆', label: 'Gold', - textColor: 'text-yellow-400', description: 'Top 15% - Outstanding community', }; case 'silver': return { - icon: '🥈', label: 'Silver', - textColor: 'text-gray-400', description: 'Top 30% - Excellent community', }; case 'bronze': return { - icon: '🥉', label: 'Bronze', - textColor: 'text-orange-400', description: 'Top 50% - Valued community', }; default: diff --git a/src/app/communities/page.tsx b/src/app/communities/page.tsx index 2a1783e..62ac8fc 100644 --- a/src/app/communities/page.tsx +++ b/src/app/communities/page.tsx @@ -10,6 +10,15 @@ import { CommunityList } from '@/components/communities/CommunityList'; import { CommunityStats } from '@/components/communities/CommunityStats'; import { CommunitySortDropdown } from '@/components/communities/CommunitySortDropdown'; import { AddCommunityCTA } from '@/components/communities/AddCommunityCTA'; +import { + OrbitMarks, + Panel, + PanelActions, + PanelButton, + PanelEyebrow, + PanelSub, +} from '@/components/panels/panel'; +import { PanelsFooter } from '@/components/panels/panels-footer'; import './leaflet.css'; export const metadata = { @@ -19,128 +28,115 @@ export const metadata = { export default function CommunitiesPage() { return ( -
- {/* Hero Section */} -
-
-
-

- Find Your React Community -

-

- Connect with React developers through meetups, conferences, and - study groups around the world. -

- - -
+
+ + +
+

+ Find Your React Community +

+

+ Connect with React developers through meetups, conferences, and + study groups around the world. +

+ + + Explore Communities + + + Start a Community + +
-
- - {/* Stats Bar - Dynamic from Redis */} -
-
- }> - + + + + By the numbers + }> + + + + + + Communities worldwide + Click any marker to learn more about a community +
+ }> +
-
+ + + + {/* + * Not the Panel primitive: its overflow-hidden would defeat the sticky + * filters and clip the sort dropdown menus, so this section recreates the + * paper surface (and its tone variables) with overflow left visible. + */} +
+ All communities +
+ - {/* Map Section */} -
-
-
-

- Communities Worldwide -

-

- Click any marker to learn more about a community -

-
+
+
+ +
-
- }> - + }> + -
- - {/* Add Community CTA */} - +
- {/* Main Content: Filters + List */} -
-
-
- {/* Filters Sidebar */} - - - {/* Community List */} -
-
-

- All Communities -

- -
- - }> - - -
+ +
+
+

+ Don't See a Community Near You? +

+

+ Starting a React community is easier than you think. We provide + resources, templates, and support to help you succeed. +

+ + + Start Your Own Community + +
-
+ - {/* CTA Section */} -
-
-

- Don't See a Community Near You? -

-

- Starting a React community is easier than you think. We provide - resources, templates, and support to help you succeed. -

- - Start Your Own Community - -
-
+
); } function StatsSkeleton() { return ( -
+
{[1, 2, 3, 4].map((i) => ( -
-
-
+
+
+
))}
@@ -149,31 +145,28 @@ function StatsSkeleton() { function FiltersSkeleton() { return ( -
-
-
-
-
+
+
+
+
+
); } function MapSkeleton() { return ( -
-
-
🗺️
-

Loading map...

-
+
+

Loading map...

); } function ListSkeleton() { return ( -
+
{[1, 2, 3, 4, 5].map((i) => ( -
+
))}
); diff --git a/src/app/globals.css b/src/app/globals.css index bdae34a..b1ebb39 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -429,3 +429,79 @@ a.inline-flex { .animate-page-appear { animation: page-appear 0.8s cubic-bezier(0.16, 1, 0.3, 1) both; } + +/* ===== Panels homepage (additive only below this line) ===== */ + +/* Geist Mono for the metric values; Tailwind's font-mono is intentionally not remapped. */ +.font-mono-panels { + font-family: var(--font-geist-mono), ui-monospace, "SF Mono", Menlo, monospace; +} + +/* + * Unlayered on purpose: the sitewide :where(a, button, ...) transition rules above + * are unlayered and therefore beat Tailwind's layered duration utilities, so the + * panels' 0.15s motion has to be unlayered too. `translate` is listed alongside + * `transform` because Tailwind v4 translate utilities use the translate property. + */ +.panels-anim { + transition-property: background-color, color, border-color; + transition-duration: 0.15s; + transition-timing-function: ease; +} + +.panels-anim-arrow { + transition-property: transform, translate; + transition-duration: 0.15s; + transition-timing-function: ease; +} + +@media (prefers-reduced-motion: reduce) { + .panels-anim, + .panels-anim-arrow, + .panels-press { + transition: none; + } + .panels-press:active { + scale: 1; + } +} + +/* + * Reserve the scrollbar gutter on every page so the fixed header and centered + * content keep identical width and position whether or not the page scrolls. + */ +html { + scrollbar-gutter: stable; +} + +/* Headings avoid one-word last lines; body text avoids orphans. */ +h1, +h2, +h3 { + text-wrap: balance; +} + +p { + text-wrap: pretty; +} + +/* Stable digit widths for dynamic numbers that are not already monospace. */ +.panels-nums { + font-variant-numeric: tabular-nums; +} + +/* + * Tactile press for buttons (0.96 per the interaction conventions; anything + * smaller reads as exaggerated). Unlayered for the same cascade reason as + * .panels-anim, and scoped to scale so it stays interruptible and composable + * with the color transitions. + */ +.panels-press { + transition-property: scale; + transition-duration: 0.1s; + transition-timing-function: ease; +} + +.panels-press:active { + scale: 0.96; +} diff --git a/src/app/impact/page.tsx b/src/app/impact/page.tsx index 487b859..b607448 100644 --- a/src/app/impact/page.tsx +++ b/src/app/impact/page.tsx @@ -1,9 +1,16 @@ import type { Metadata } from "next"; -import { ButtonLink } from "@/components/ui/button"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { Footer } from "@/components/layout/footer"; +import { BarChart3, BookOpen, CircleDollarSign, Globe, MessageSquare, Users } from "lucide-react"; + import { EcosystemLibraries } from "@/components/home/ecosystem-libraries"; +import { + OrbitMarks, + Panel, + PanelActions, + PanelButton, + PanelEyebrow, + PanelSub, +} from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; import { ecosystemLibraries } from "@/lib/maintainer-tiers"; export const metadata: Metadata = { @@ -11,186 +18,150 @@ export const metadata: Metadata = { description: "See how React Foundation funding supports the ecosystem with transparent quarterly reports.", }; +const REPORT_CONTENTS = [ + { + icon: CircleDollarSign, + title: "Revenue Details", + description: "Total revenue generated from all sources", + }, + { + icon: Users, + title: "Maintainer Funding", + description: "Breakdown of funding by library and maintainer", + }, + { + icon: BookOpen, + title: "Education Initiatives", + description: "Tutorials, docs, and learning resources supported", + }, + { + icon: Globe, + title: "Accessibility", + description: "Global accessibility improvements funded", + }, + { + icon: BarChart3, + title: "Impact Metrics", + description: "Downloads, usage, and ecosystem growth data", + }, + { + icon: MessageSquare, + title: "Community Feedback", + description: "Testimonials from maintainers and contributors", + }, +]; + +const DISTRIBUTION_STEPS = [ + { + title: "Contribution Tracking", + description: + "We track pull requests, issues, and commits across all 54 supported libraries using GitHub's GraphQL API.", + }, + { + title: "Score Calculation", + description: + "Contributions are weighted (PRs × 8 + Issues × 3 + Commits × 1) to calculate fair distribution ratios.", + }, + { + title: "Fund Distribution", + description: + "100% of profits are distributed quarterly based on contribution scores and library impact metrics.", + }, +]; + export default function ImpactPage() { return ( -
-
-
-
+
+ + +
+

+ Our Impact +

+

+ Full transparency on how your support funds the React ecosystem. Every + contribution is tracked and reported publicly. +

+
+
-
-
- {/* Hero */} -
- 100% Transparent · Quarterly Reports · Real Impact -
-

- Our Impact -

-

- Full transparency on how your support funds the React ecosystem. Every - contribution is tracked and reported publicly. -

-
-
- - {/* Coming Soon */} - -
-
-
- - - -
-

- First Report Coming Soon -

-

- Our inaugural quarterly impact report will be published once the store - launches. Each report will provide complete transparency into fund - distribution. + + First report coming soon + + Our inaugural quarterly impact report will be published once the store + launches. Each report will provide complete transparency into fund + distribution. + +

+ {REPORT_CONTENTS.map((item) => ( +
+
+ ))} +
+ -
-
-
💰
-

Revenue Details

-

- Total revenue generated from all sources -

-
-
-
👥
-

Maintainer Funding

-

- Breakdown of funding by library and maintainer -

-
-
-
📚
-

Education Initiatives

-

- Tutorials, docs, and learning resources supported -

-
-
-
🌍
-

Accessibility

-

- Global accessibility improvements funded -

-
-
-
📊
-

Impact Metrics

-

- Downloads, usage, and ecosystem growth data -

-
-
-
💬
-

Community Feedback

-

- Testimonials from maintainers and contributors -

-
-
-
-
- - {/* Ecosystem Libraries */} - - - {/* How Funds are Distributed */} - -
-

- How Funds are Distributed -

-

- We use a transparent, metrics-based approach to ensure fair distribution - of funds to maintainers across the React ecosystem. -

- -
-
-
- 1 -
-

- Contribution Tracking -

-

- We track pull requests, issues, and commits across all 54 supported - libraries using GitHub's GraphQL API. -

-
- -
-
- 2 -
-

Score Calculation

-

- Contributions are weighted (PRs × 8 + Issues × 3 + Commits × 1) to - calculate fair distribution ratios. -

-
+ -
-
- 3 -
-

Fund Distribution

-

- 100% of profits are distributed quarterly based on contribution scores - and library impact metrics. -

-
+ + How funds are distributed + + We use a transparent, metrics-based approach to ensure fair distribution of + funds to maintainers across the React ecosystem. + +
+ {DISTRIBUTION_STEPS.map((step, index) => ( +
+ + {index + 1} + +
+

{step.title}

+

+ {step.description} +

-
-
+
+ ))} +
+ - {/* CTA */} - -
-

- Support the Ecosystem -

-

- Every purchase directly supports React ecosystem maintainers. Shop the - store to make an impact today. -

-
- - Shop the Store - - - Learn More - -
-
-
- -
+ + Support the ecosystem +

+ Every purchase directly supports React ecosystem maintainers. Shop the store + to make an impact today. +

+ + + Shop the Store + + + Learn More + + +
-
+
); } diff --git a/src/app/libraries/page.tsx b/src/app/libraries/page.tsx index 1944bee..08f6937 100644 --- a/src/app/libraries/page.tsx +++ b/src/app/libraries/page.tsx @@ -12,13 +12,14 @@ import { useCollectionStatus, } from '@/lib/ris'; import { RISLibraryRankings } from '@/components/ris/ris-library-rankings'; -import { RFDS } from '@/components/rfds'; +import { Panel, PanelButton, PanelEyebrow, PanelPlainLink, PanelSub, RowList } from '@/components/panels/panel'; +import { PanelsFooter } from '@/components/panels/panels-footer'; import Link from 'next/link'; export default function LibrariesPage() { // Try to fetch real data from API const { allocation: realAllocation, isLoading, isError } = useRISAllocationFromAPI(); - const { lastUpdated, currentQuarter } = useCollectionStatus(); + const { lastUpdated } = useCollectionStatus(); // Fall back to sample data if real data not available const useSampleData = isError || !realAllocation; @@ -31,28 +32,27 @@ export default function LibrariesPage() { const avgRIS = allocation.libraries.reduce((sum, lib) => sum + lib.ris, 0) / totalLibraries; return ( -
-
- {/* Header */} -
-
- - ← How Scoring Works - -
-

+
+ +
+

Library Impact Dashboard

-

- React ecosystem libraries ranked by their impact score +

+ React ecosystem libraries ranked by their impact score.

+
+ How Scoring Works +
+
- {/* Summary Stats */} -
+ + By the numbers + -
+ + - {/* Component Stats */} -
-

- Component Score Averages -

-
- {componentStats.map((stat) => ( -
-
{stat.label}
-
- {(stat.avg * 100).toFixed(1)}% -
-
- Range: {(stat.min * 100).toFixed(0)}% - {(stat.max * 100).toFixed(0)}% -
+ + Component score averages + Average RIS components across the currently loaded library set. +
+ {componentStats.map((stat) => ( +
+
{stat.label}
+
+ {(stat.avg * 100).toFixed(1)}%
- ))} -
+
+ Range: {(stat.min * 100).toFixed(0)}% - {(stat.max * 100).toFixed(0)}% +
+
+ ))}
+ - {/* Loading State */} - {isLoading && ( -
-
-

Loading RIS data...

+ {isLoading && ( + +
+ Loading data +
+

Loading RIS data...

- )} +
+ )} - {/* Data Source Banner */} - {!isLoading && ( -
+
{useSampleData ? '⚠️' : '✓'} -
+
{useSampleData ? ( <> -

Using Sample Data

+

Using Sample Data

Real RIS data not yet available. This dashboard shows sample data demonstrating how the React Impact Score (RIS) system evaluates libraries across 5 key components.

To use real data, configure your GitHub PAT and Redis URL, then run data collection - via POST /api/ris/collect + via POST /api/ris/collect

) : ( <> -

Using Real Data

+

Using Real Data

This dashboard displays actual metrics collected from GitHub, NPM, CDN providers, and OSSF Scorecard for {allocation.libraries.length} React ecosystem libraries.

{lastUpdated && ( -

+

Last updated: {new Date(lastUpdated).toLocaleString()}

)} )}

- + Learn more about how scoring works →

- )} + + )} - {/* Library Rankings */} -
-
-

- Library Rankings -

-
- {totalLibraries} libraries · Total pool: ${allocation.total_pool_usd.toLocaleString()} -
+ +
+
+ Library rankings + Scores and allocation estimates for the current RIS period.
+
+ {totalLibraries} libraries · Total pool: ${allocation.total_pool_usd.toLocaleString()} +
+
+
+
- {/* Methodology Note */} -
-

- Scores are calculated using winsorized normalization to reduce outlier impact, - with EMA smoothing for quarter-to-quarter stability. -

-

- All data is transparent and reproducible. See the{' '} - - scoring documentation - {' '} - for full methodology. -

+ +
+
+ Methodology +

+ Scores are calculated using winsorized normalization to reduce outlier impact, + with EMA smoothing for quarter-to-quarter stability. +

+

+ All data is transparent and reproducible. See the scoring documentation for full methodology. +

+
+ + View scoring docs +
-
+ + +
); } @@ -198,13 +206,14 @@ function StatCard({ highlight?: boolean; }) { return ( - +
+
+

{label}

+

{subtext}

+
+
+ {value} +
+
); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 4e3efbc..0d879d7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,37 +1,30 @@ import type { Metadata } from "next"; -import { Footer } from "@/components/layout/footer"; -import { FoundationHero } from "@/components/home/foundation-hero"; -import { MissionStatement } from "@/components/home/mission-statement"; -import { ThreePillars } from "@/components/home/three-pillars"; -import { BecomeContributor } from "@/components/home/become-contributor"; -import { JoinMovementCTA } from "@/components/home/join-movement-cta"; -import { FoundingMembers } from "@/components/home/founding-members"; + +import { PanelContribute } from "@/components/home/panels/panel-contribute"; +import { PanelHero } from "@/components/home/panels/panel-hero"; +import { PanelJoin } from "@/components/home/panels/panel-join"; +import { PanelMembers } from "@/components/home/panels/panel-members"; +import { PanelMetrics } from "@/components/home/panels/panel-metrics"; +import { PanelMission } from "@/components/home/panels/panel-mission"; +import { PanelPillars } from "@/components/home/panels/panel-pillars"; +import { PanelsFooter } from "@/components/panels/panels-footer"; export const metadata: Metadata = { - title: "React Foundation", - description: "Supporting the React ecosystem through community funding and governance.", + title: "React Foundation", + description: "Supporting the React ecosystem through community funding and governance.", }; export default function FoundationHome() { - return ( -
- {/* Background gradient */} -
-
-
- -
-
- - - - - - -
-
- -
-
- ); + return ( +
+ + + + + + + + +
+ ); } diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index 8b4f69f..2f355a1 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -1,174 +1,173 @@ /* eslint-disable react/no-unescaped-entities */ import type { Metadata } from "next"; -import { Footer } from "@/components/layout/footer"; + +import { Panel } from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; export const metadata: Metadata = { - title: "Privacy Policy", - description: "Privacy Policy for React Foundation", + title: "Privacy Policy", + description: "Privacy Policy for React Foundation", }; export default function PrivacyPage() { - return ( -
-
-
-
- -
-
-

Privacy Policy

-

- Last updated: October 21, 2025 -

- -
-
-

1. Introduction

-

- React Foundation ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website and use our services. -

-

- Please read this Privacy Policy carefully. If you do not agree with the terms of this Privacy Policy, please do not access the site. -

-
- -
-

2. Information We Collect

- -

2.1 Personal Information

-

- When you authenticate using GitHub or GitLab OAuth, we collect: -

-
    -
  • Your name
  • -
  • Your email address
  • -
  • Your GitHub or GitLab username
  • -
  • Your public profile information from the respective platform
  • -
- -

2.2 Local Storage

-

- We use browser local storage to save your contributor username preference for convenience. This data is stored only on your device and is not transmitted to our servers. -

- -

2.3 Session Data

-

- We use session cookies to maintain your authenticated state. These are essential for the functionality of the authentication system. -

-
- -
-

3. How We Use Your Information

-

- We use the information we collect in the following ways: -

-
    -
  • To provide, operate, and maintain our website
  • -
  • To authenticate and authorize your access to features
  • -
  • To display your contributor status and progress
  • -
  • To personalize your experience on our website
  • -
  • To communicate with you about updates and announcements
  • -
-
- -
-

4. Data Sharing and Disclosure

-

- We do not share, sell, rent, or trade your personal information with third parties for their commercial purposes. -

-

- We may share information only in the following limited circumstances: -

-
    -
  • Service Providers: We use third-party services (GitHub, GitLab for authentication; Shopify for e-commerce) that may have access to your information to perform tasks on our behalf.
  • -
  • Legal Requirements: We may disclose your information if required to do so by law or in response to valid requests by public authorities.
  • -
-
- -
-

5. Third-Party Services

- -

5.1 Authentication

-

- We use GitHub and GitLab OAuth for authentication. When you authenticate, you are subject to their respective privacy policies: -

- - -

5.2 E-commerce

-

- Our online store is powered by Shopify. When you make a purchase, Shopify collects and processes your payment and shipping information. Please review Shopify's Privacy Policy for more information. -

-
- -
-

6. Tracking and Analytics

-

- We do not use any tracking technologies, analytics tools, or third-party advertising services. We do not track your browsing behavior across websites or collect data for advertising purposes. -

-
- -
-

7. Data Security

-

- We implement appropriate technical and organizational security measures to protect your personal information. However, please note that no method of transmission over the internet or method of electronic storage is 100% secure. -

-

- Your authentication is handled by NextAuth.js, a secure authentication library, and we rely on GitHub and GitLab's OAuth implementations for secure authentication. -

-
- -
-

8. Data Retention

-

- We retain your personal information only for as long as necessary to provide you with our services and as described in this Privacy Policy. Session data is retained only for the duration of your authenticated session. -

-
- -
-

9. Your Rights

-

- You have the right to: -

-
    -
  • Access the personal information we hold about you
  • -
  • Request correction of inaccurate information
  • -
  • Request deletion of your personal information
  • -
  • Revoke OAuth access through your GitHub or GitLab account settings
  • -
  • Clear local storage data stored in your browser
  • -
-
- -
-

10. Children's Privacy

-

- Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us. -

-
- -
-

11. Changes to This Privacy Policy

-

- We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date at the top of this Privacy Policy. -

-

- You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. -

-
- -
-

12. Contact Us

-

- If you have any questions about this Privacy Policy, please contact us through our GitHub repository or official communication channels. -

-
-
-
-
- -
-
- ); + return ( +
+ +

+ Privacy Policy +

+

+ Last updated: October 21, 2025 +

+ +
+

1. Introduction

+

+ React Foundation ("we", "our", or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you visit our website and use our services. +

+

+ Please read this Privacy Policy carefully. If you do not agree with the terms of this Privacy Policy, please do not access the site. +

+
+ +
+

2. Information We Collect

+ +

2.1 Personal Information

+

+ When you authenticate using GitHub or GitLab OAuth, we collect: +

+
    +
  • Your name
  • +
  • Your email address
  • +
  • Your GitHub or GitLab username
  • +
  • Your public profile information from the respective platform
  • +
+ +

2.2 Local Storage

+

+ We use browser local storage to save your contributor username preference for convenience. This data is stored only on your device and is not transmitted to our servers. +

+ +

2.3 Session Data

+

+ We use session cookies to maintain your authenticated state. These are essential for the functionality of the authentication system. +

+
+ +
+

3. How We Use Your Information

+

+ We use the information we collect in the following ways: +

+
    +
  • To provide, operate, and maintain our website
  • +
  • To authenticate and authorize your access to features
  • +
  • To display your contributor status and progress
  • +
  • To personalize your experience on our website
  • +
  • To communicate with you about updates and announcements
  • +
+
+ +
+

4. Data Sharing and Disclosure

+

+ We do not share, sell, rent, or trade your personal information with third parties for their commercial purposes. +

+

+ We may share information only in the following limited circumstances: +

+
    +
  • Service Providers: We use third-party services (GitHub, GitLab for authentication; Shopify for e-commerce) that may have access to your information to perform tasks on our behalf.
  • +
  • Legal Requirements: We may disclose your information if required to do so by law or in response to valid requests by public authorities.
  • +
+
+ +
+

5. Third-Party Services

+ +

5.1 Authentication

+

+ We use GitHub and GitLab OAuth for authentication. When you authenticate, you are subject to their respective privacy policies: +

+ + +

5.2 E-commerce

+

+ Our online store is powered by Shopify. When you make a purchase, Shopify collects and processes your payment and shipping information. Please review Shopify's Privacy Policy for more information. +

+
+ +
+

6. Tracking and Analytics

+

+ We do not use any tracking technologies, analytics tools, or third-party advertising services. We do not track your browsing behavior across websites or collect data for advertising purposes. +

+
+ +
+

7. Data Security

+

+ We implement appropriate technical and organizational security measures to protect your personal information. However, please note that no method of transmission over the internet or method of electronic storage is 100% secure. +

+

+ Your authentication is handled by NextAuth.js, a secure authentication library, and we rely on GitHub and GitLab's OAuth implementations for secure authentication. +

+
+ +
+

8. Data Retention

+

+ We retain your personal information only for as long as necessary to provide you with our services and as described in this Privacy Policy. Session data is retained only for the duration of your authenticated session. +

+
+ +
+

9. Your Rights

+

+ You have the right to: +

+
    +
  • Access the personal information we hold about you
  • +
  • Request correction of inaccurate information
  • +
  • Request deletion of your personal information
  • +
  • Revoke OAuth access through your GitHub or GitLab account settings
  • +
  • Clear local storage data stored in your browser
  • +
+
+ +
+

10. Children's Privacy

+

+ Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us. +

+
+ +
+

11. Changes to This Privacy Policy

+

+ We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page and updating the "Last updated" date at the top of this Privacy Policy. +

+

+ You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. +

+
+ +
+

12. Contact Us

+

+ If you have any questions about this Privacy Policy, please contact us through our GitHub repository or official communication channels. +

+
+
+ + +
+ ); } diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index 1392c53..316654f 100644 --- a/src/app/terms/page.tsx +++ b/src/app/terms/page.tsx @@ -1,114 +1,113 @@ /* eslint-disable react/no-unescaped-entities */ import type { Metadata } from "next"; -import { Footer } from "@/components/layout/footer"; + +import { Panel } from "@/components/panels/panel"; +import { PanelsFooter } from "@/components/panels/panels-footer"; export const metadata: Metadata = { - title: "Terms of Service", - description: "Terms of Service for React Foundation", + title: "Terms of Service", + description: "Terms of Service for React Foundation", }; export default function TermsPage() { - return ( -
-
-
-
- -
-
-

Terms of Service

-

- Last updated: October 21, 2025 -

+ return ( +
+ +

+ Terms of Service +

+

+ Last updated: October 21, 2025 +

-
-
-

1. Acceptance of Terms

-

- By accessing and using the React Foundation website (the "Service"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to these Terms of Service, please do not use the Service. -

-
+
+

1. Acceptance of Terms

+

+ By accessing and using the React Foundation website (the "Service"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to these Terms of Service, please do not use the Service. +

+
-
-

2. Description of Service

-

- React Foundation provides a platform to support the React ecosystem through community funding, transparent governance, and official merchandise. The Service includes: -

-
    -
  • Information about React Foundation and its mission
  • -
  • An online store for official React Foundation merchandise
  • -
  • User authentication via GitHub and GitLab
  • -
  • Contributor progress tracking and status information
  • -
  • Community updates and announcements
  • -
-
+
+

2. Description of Service

+

+ React Foundation provides a platform to support the React ecosystem through community funding, transparent governance, and official merchandise. The Service includes: +

+
    +
  • Information about React Foundation and its mission
  • +
  • An online store for official React Foundation merchandise
  • +
  • User authentication via GitHub and GitLab
  • +
  • Contributor progress tracking and status information
  • +
  • Community updates and announcements
  • +
+
-
-

3. User Accounts

-

- To access certain features of the Service, you may be required to authenticate using your GitHub or GitLab account. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. -

-
+
+

3. User Accounts

+

+ To access certain features of the Service, you may be required to authenticate using your GitHub or GitLab account. You are responsible for maintaining the confidentiality of your account credentials and for all activities that occur under your account. +

+
-
-

4. Acceptable Use

-

- You agree to use the Service only for lawful purposes and in a way that does not infringe the rights of, restrict, or inhibit anyone else's use and enjoyment of the Service. Prohibited behavior includes: -

-
    -
  • Harassing or causing distress or inconvenience to any other user
  • -
  • Transmitting obscene or offensive content
  • -
  • Disrupting the normal flow of dialogue within the Service
  • -
  • Attempting to gain unauthorized access to the Service or its systems
  • -
-
+
+

4. Acceptable Use

+

+ You agree to use the Service only for lawful purposes and in a way that does not infringe the rights of, restrict, or inhibit anyone else's use and enjoyment of the Service. Prohibited behavior includes: +

+
    +
  • Harassing or causing distress or inconvenience to any other user
  • +
  • Transmitting obscene or offensive content
  • +
  • Disrupting the normal flow of dialogue within the Service
  • +
  • Attempting to gain unauthorized access to the Service or its systems
  • +
+
-
-

5. Intellectual Property

-

- The Service and its original content, features, and functionality are owned by React Foundation and are protected by international copyright, trademark, patent, trade secret, and other intellectual property laws. The React logo and other marks are trademarks of Meta Platforms, Inc., and are used with permission. -

-
+
+

5. Intellectual Property

+

+ The Service and its original content, features, and functionality are owned by React Foundation and are protected by international copyright, trademark, patent, trade secret, and other intellectual property laws. The React logo and other marks are trademarks of Meta Platforms, Inc., and are used with permission. +

+
-
-

6. Purchases and Payment

-

- If you wish to purchase any product or service made available through the Service, you may be asked to supply certain information relevant to your purchase. Payment processing is handled by third-party payment processors. We are not responsible for the security of payment information submitted to these third parties. -

-
+
+

6. Purchases and Payment

+

+ If you wish to purchase any product or service made available through the Service, you may be asked to supply certain information relevant to your purchase. Payment processing is handled by third-party payment processors. We are not responsible for the security of payment information submitted to these third parties. +

+
-
-

7. Limitation of Liability

-

- In no event shall React Foundation, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential, or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from your access to or use of or inability to access or use the Service. -

-
+
+

7. Limitation of Liability

+

+ In no event shall React Foundation, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential, or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from your access to or use of or inability to access or use the Service. +

+
-
-

8. Disclaimer

-

- Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement, or course of performance. -

-
+
+

8. Disclaimer

+

+ Your use of the Service is at your sole risk. The Service is provided on an "AS IS" and "AS AVAILABLE" basis. The Service is provided without warranties of any kind, whether express or implied, including, but not limited to, implied warranties of merchantability, fitness for a particular purpose, non-infringement, or course of performance. +

+
-
-

9. Changes to Terms

-

- We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion. -

-
+
+

9. Changes to Terms

+

+ We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion. +

+
-
-

10. Contact Information

-

- If you have any questions about these Terms of Service, please contact us through our GitHub repository or official communication channels. -

-
-
-
-
+
+

10. Contact Information

+

+ If you have any questions about these Terms of Service, please contact us through our GitHub repository or official communication channels. +

+
+ -
-
- ); + +
+ ); } diff --git a/src/app/updates/[slug]/page.tsx b/src/app/updates/[slug]/page.tsx index 02db4ef..dc22ccd 100644 --- a/src/app/updates/[slug]/page.tsx +++ b/src/app/updates/[slug]/page.tsx @@ -1,110 +1,110 @@ import type { Metadata } from "next"; -import { notFound } from "next/navigation"; -import { Pill } from "@/components/ui/pill"; -import { getUpdateBySlug, getAllUpdates } from "@/lib/updates"; -import { getAuthorBySlug } from "@/lib/authors"; -import { MDXRemote } from "next-mdx-remote/rsc"; import Image from "next/image"; import Link from "next/link"; +import { notFound } from "next/navigation"; +import { MDXRemote } from "next-mdx-remote/rsc"; + +import { Panel, PanelEyebrow, PanelPlainLink } from "@/components/panels/panel"; +import { getAuthorBySlug } from "@/lib/authors"; +import { getAllUpdates, getUpdateBySlug } from "@/lib/updates"; type UpdatePageProps = { - params: Promise<{ - slug: string; - }>; + params: Promise<{ + slug: string; + }>; }; export async function generateStaticParams() { - const updates = getAllUpdates(); - return updates.map((update) => ({ - slug: update.slug, - })); + const updates = getAllUpdates(); + + return updates.map((update) => ({ + slug: update.slug, + })); } export async function generateMetadata({ - params, + params, }: UpdatePageProps): Promise { - const { slug } = await params; - const update = getUpdateBySlug(slug); + const { slug } = await params; + const update = getUpdateBySlug(slug); - if (!update) { - return { - title: "Update not found", - }; - } + if (!update) { + return { + title: "Update not found", + }; + } + + return { + title: update.metadata.title, + description: update.metadata.description, + }; +} - return { - title: update.metadata.title, - description: update.metadata.description, - }; +function formatDate(date: string) { + return new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); } export default async function UpdatePage({ params }: UpdatePageProps) { - const { slug } = await params; - const update = getUpdateBySlug(slug); + const { slug } = await params; + const update = getUpdateBySlug(slug); - if (!update) { - notFound(); - } + if (!update) { + notFound(); + } - const author = getAuthorBySlug(update.metadata.author); + const author = getAuthorBySlug(update.metadata.author); - return ( -
- {/* Post Header */} -
- - Update ·{" "} - {new Date(update.metadata.date).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - })} - -

- {update.metadata.title} -

+ return ( +
+ + {formatDate(update.metadata.date)} +

+ {update.metadata.title} +

- {/* Author Info */} - {author && ( -
- {author.avatar && ( -
- {author.name} -
- )} -
- - {author.name} - -

{author.title}

-
-
- )} -
+ {author && ( +
+ {author.avatar && ( +
+ {author.name} +
+ )} +
+ + {author.name} + +

{author.title}

+
+
+ )} + - {/* MDX Content with prose */} -
- -
+
+ +
- {/* Back Link */} -
- - ← Back to all updates - -
-
- ); + +

+ Update navigation +

+ Back to all updates +
+ + ); } diff --git a/src/app/updates/layout.tsx b/src/app/updates/layout.tsx index 7fe651f..00f828e 100644 --- a/src/app/updates/layout.tsx +++ b/src/app/updates/layout.tsx @@ -1,21 +1,16 @@ -import { Footer } from "@/components/layout/footer"; +import type { ReactNode } from "react"; + +import { PanelsFooter } from "@/components/panels/panels-footer"; export default function UpdatesLayout({ - children, + children, }: { - children: React.ReactNode; + children: ReactNode; }) { - return ( -
-
-
-
- -
- {children} -
- -
-
- ); + return ( +
+ {children} + +
+ ); } diff --git a/src/app/updates/page.tsx b/src/app/updates/page.tsx index 0e26738..e5091b0 100644 --- a/src/app/updates/page.tsx +++ b/src/app/updates/page.tsx @@ -1,69 +1,74 @@ import type { Metadata } from "next"; -import Link from "next/link"; -import { Pill } from "@/components/ui/pill"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; -import { getAllUpdates } from "@/lib/updates"; + +import { Panel, PanelEyebrow, PanelSub, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; import { getAuthorBySlug } from "@/lib/authors"; +import { getAllUpdates } from "@/lib/updates"; export const metadata: Metadata = { - title: "Updates", - description: "Latest news and announcements from the React Foundation.", + title: "Updates", + description: "Latest news and announcements from the React Foundation.", }; +function formatDate(date: string) { + return new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + export default function UpdatesPage() { - const updates = getAllUpdates(); - return ( -
- {/* Hero */} -
- Latest News · Announcements · Community Updates -
-

- Updates -

-

- Stay informed about the latest news, announcements, and initiatives from - the React Foundation. -

-
-
+ const updates = getAllUpdates(); + + return ( +
+ +

+ Updates +

+ + Stay informed about the latest news, announcements, and initiatives from the + React Foundation. + +
- {/* Updates List */} - -
- {updates.map((update, idx) => { - const author = getAuthorBySlug(update.metadata.author); + + Latest posts + + {updates.map((update) => { + const author = getAuthorBySlug(update.metadata.author); - return ( - - -
- - · - {author?.name || update.metadata.author} -
-

- {update.metadata.title} -

-

{update.metadata.description}

-
- Read more → -
- -
- ); - })} -
-
-
- ); + return ( + + +
+

+ {update.metadata.title} +

+

+ {update.metadata.description} +

+
+ + {author?.name || update.metadata.author} + +
+ ); + })} + + +
+ ); } diff --git a/src/components/communities/AddCommunityCTA.tsx b/src/components/communities/AddCommunityCTA.tsx index e2d4ae0..bc94bfa 100644 --- a/src/components/communities/AddCommunityCTA.tsx +++ b/src/components/communities/AddCommunityCTA.tsx @@ -8,11 +8,11 @@ import Link from 'next/link'; export function AddCommunityCTA() { return (
-

+

Don't see your community listed?{' '} Add it now → diff --git a/src/components/communities/CommunityFilters.tsx b/src/components/communities/CommunityFilters.tsx index 174529b..e0914fa 100644 --- a/src/components/communities/CommunityFilters.tsx +++ b/src/components/communities/CommunityFilters.tsx @@ -7,7 +7,7 @@ import { useEffect, useRef, useState, type RefObject } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { RFDS } from '@/components/rfds'; +import { Search } from 'lucide-react'; import { SortDropdown } from '@/components/ui/sort-dropdown'; import type { CommunityFilters as Filters, CommunityStatusFilter, EventType } from '@/types/community'; @@ -19,6 +19,9 @@ const STATUS_OPTIONS: Array<{ value: CommunityStatusFilter; label: string }> = [ { value: 'inactive', label: 'Inactive' }, ]; +const FOCUS_RING = + 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[#16181D]'; + function useClearDebounceOnUnmount(timer: RefObject | null>) { useEffect(() => { function clearDebounce() { @@ -126,27 +129,24 @@ export function CommunityFilters() { (filters.status && filters.status !== 'all'); return ( -

- {/* Header */} +
-

Filters

+

Filters

{hasActiveFilters && ( - Clear all - + )}
- {/* Status shortcuts */}
- +
{STATUS_OPTIONS.map((option) => { const selected = (filters.status || 'all') === option.value; @@ -157,8 +157,8 @@ export function CommunityFilters() { type="button" onClick={() => updateFilter('status', option.value)} className={selected - ? 'rounded-full bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition' - : 'rounded-full border border-border bg-card px-3 py-1.5 text-sm font-medium text-muted-foreground transition hover:border-primary hover:text-primary' + ? `panels-anim rounded-full border border-[#16181D] bg-[#16181D] px-3 py-1.5 text-[13px] font-medium text-[#F6F7F9]! ${FOCUS_RING}` + : `panels-anim rounded-full border border-[rgba(22,24,29,0.2)] px-3 py-1.5 text-[13px] font-medium text-[#5E687E] hover:bg-[rgba(22,24,29,0.08)] hover:text-[#16181D] ${FOCUS_RING}` } > {option.label} @@ -168,34 +168,43 @@ export function CommunityFilters() {
- {/* Search */}
-
- {/* Event Types */}
- +
{(['meetup', 'conference', 'workshop', 'hackathon', 'virtual'] as EventType[]).map( (type) => ( - @@ -204,44 +213,30 @@ export function CommunityFilters() {
- {/* CoIS Tier */}
+ + CoIS Tier + updateFilter('cois_tier', value as Filters['cois_tier'])} />
- {/* Toggles */} - {/*
- - -
*/} - {/* Apply button (for mobile) */} - +
); } diff --git a/src/components/communities/CommunityList.tsx b/src/components/communities/CommunityList.tsx index 8ab0392..6f57535 100644 --- a/src/components/communities/CommunityList.tsx +++ b/src/components/communities/CommunityList.tsx @@ -1,6 +1,6 @@ /** * Community List Component - * List view of communities with cards + * List view of communities with ruled rows */ 'use client'; @@ -8,8 +8,8 @@ import useSWR from 'swr'; import Link from 'next/link'; import { useSearchParams } from 'next/navigation'; -import { getCommunityHostLabel } from '@/lib/community-host'; -import { VerificationBadge } from './VerificationBadge'; +import type { ReactNode } from 'react'; +import { RowArrow, RowList } from '@/components/panels/panel'; import type { Community } from '@/types/community'; // Fetcher for SWR @@ -135,9 +135,9 @@ export function CommunityList() { if (isLoading) { return ( -
+
{[1, 2, 3].map((i) => ( -
+
))}
); @@ -145,162 +145,116 @@ export function CommunityList() { if (error) { return ( -
-

Failed to load communities

-

Please try refreshing the page

+
+

Failed to load communities

+

Please try refreshing the page

); } return ( -
-

+

+

Showing {communities.length} React communities worldwide

- {communities.map((community) => ( - - ))} + + {communities.map((community) => ( + + ))} + {communities.length === 0 && ( -
-

No communities found

+
+

No communities found

)}
); } -function CommunityCard({ community }: { community: Community }) { - const tierBadge = getTierBadge(community.cois_tier); - +/* + * Not the shared Row: the row hosts secondary external links, and anchors + * cannot nest inside the Row anchor, so the hover surface is a div and the + * detail link wraps only the title block and the trailing arrow. + */ +function CommunityRow({ community }: { community: Community }) { + const detailHref = `/communities/${community.slug}`; return ( -
-
- {/* Main Info */} -
-
-
-
-

- {community.name} -

- -
-

- {community.city} - {community.region && `, ${community.region}`}, {community.country} -

-
- {tierBadge && ( - - {tierBadge.icon} {tierBadge.label} - - )} -
- -

- {community.description} -

- - {/* Stats */} -
-
- 👥 - {community.member_count.toLocaleString()} members -
-
- 📅 - {community.meeting_frequency} -
- {community.last_event_date && ( -
- 🎯 - Last event: {new Date(community.last_event_date).toLocaleDateString()} -
- )} -
- - {/* Event Types */} -
- {community.event_types.map((type) => ( - - {type} - - ))} -
-
- - {/* Actions */} -
- - View Details - - {community.meetup_url ? ( - - - - - Join on {getCommunityHostLabel(community.meetup_url)} - - ) : community.website && ( - - Visit Website → - +
+ +
+

{community.name}

+ {getVerificationLabel(community)} + {community.cois_tier && community.cois_tier !== 'none' && ( + + {community.cois_tier} + )} + {community.event_types.map((type) => ( + + {type} + + ))}
-
+

+ {community.city} + {community.region && `, ${community.region}`}, {community.country} +

+ + + {community.meetup_url && ( + + Meetup ↗ + + )} + {community.website && ( + + Website ↗ + + )} + + + {community.member_count.toLocaleString()} members + + + +
); } -function getTierBadge(tier?: string): { icon: string; label: string; className: string } | null { - switch (tier) { - case 'platinum': - return { - icon: '💎', - label: 'Platinum', - className: 'bg-gradient-to-r from-cyan-400 to-blue-400 text-white', - }; - case 'gold': - return { - icon: '🏆', - label: 'Gold', - className: 'bg-gradient-to-r from-yellow-400 to-orange-400 text-white', - }; - case 'silver': - return { - icon: '🥈', - label: 'Silver', - className: 'bg-gradient-to-r from-gray-300 to-gray-400 text-white', - }; - case 'bronze': - return { - icon: '🥉', - label: 'Bronze', - className: 'bg-gradient-to-r from-orange-300 to-orange-400 text-white', - }; - default: - return null; - } +function MonoChip({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function getVerificationLabel(community: Community): string { + const status = + community.verification_status || (community.verified ? 'verified' : 'pending'); + + return { + verified: 'Verified', + pending: 'Pending Review', + rejected: 'Not Verified', + }[status]; } diff --git a/src/components/communities/CommunityMap.tsx b/src/components/communities/CommunityMap.tsx index 77217ec..c5181b1 100644 --- a/src/components/communities/CommunityMap.tsx +++ b/src/components/communities/CommunityMap.tsx @@ -233,13 +233,12 @@ export function CommunityMap() { if (!isClient || isLoading || !L) { return ( -
+
-
🗺️
-

+

{isLoading ? 'Loading communities...' : !L ? 'Loading map library...' : 'Loading map...'}

-

+

{isLoading ? `Fetching ${communities.length} communities` : 'Initializing Leaflet'}

@@ -254,15 +253,10 @@ export function CommunityMap() { console.log('🗺️ CommunityMap: Rendering map, isClient:', isClient); return ( -
- {/* Debug indicator */} - {/*
- MAP CONTAINER VISIBLE -
*/} - +
{/* Map Legend */} -
-

CoIS Tier

+
+

CoIS Tier

@@ -299,57 +293,53 @@ export function CommunityMap() { title={`${community.name} - ${community.cois_tier || community.status}`} > -
- {/* Header with tier badge */} +
{community.cois_tier && ( -
- - {getTierIcon(community.cois_tier)} {community.cois_tier} +
+ +
)} -

+

{community.name}

-

+

{community.city} {community.region && `, ${community.region}`}, {community.country}

-

+

{community.description}

-
+
-
Members
-
+
Members
+
{community.member_count.toLocaleString()}
-
Frequency
-
+
Frequency
+
{community.meeting_frequency}
-
+
{community.event_types.map((type) => ( {type} @@ -359,11 +349,7 @@ export function CommunityMap() {
View Details @@ -372,11 +358,7 @@ export function CommunityMap() { href={community.meetup_url} target="_blank" rel="noopener noreferrer" - className="flex-shrink-0 rounded-lg px-3 py-2.5 text-sm font-semibold transition hover:opacity-90" - style={{ - backgroundColor: '#ED1C40', - color: '#ffffff' - }} + className="panels-anim flex-shrink-0 rounded-xl bg-[#ED1C40] px-3 py-2.5 text-sm font-semibold text-white! hover:bg-[#C8173A]" title={`Join on ${getCommunityHostLabel(community.meetup_url)}`} > @@ -415,21 +397,6 @@ function getTierColorHex(tier?: string, status?: string): string { } } -function getTierBadgeColor(tier: string): string { - switch (tier) { - case 'platinum': - return 'bg-gradient-to-r from-cyan-400 to-blue-400'; - case 'gold': - return 'bg-gradient-to-r from-yellow-400 to-orange-400'; - case 'silver': - return 'bg-gradient-to-r from-gray-300 to-gray-400'; - case 'bronze': - return 'bg-gradient-to-r from-orange-300 to-orange-400'; - default: - return 'bg-primary'; - } -} - function getTierIcon(tier: string, status?: string): string { // Different icons for inactive/paused if (status === 'inactive') return '⏸'; @@ -457,7 +424,7 @@ function TierLegend({ tier }: { tier: string }) { className="w-3 h-3 rounded-full" style={{ backgroundColor: getTierColorHex(tier) }} /> - {tier} + {tier}
); } diff --git a/src/components/communities/CommunitySortDropdown.tsx b/src/components/communities/CommunitySortDropdown.tsx index f8fb604..b8be536 100644 --- a/src/components/communities/CommunitySortDropdown.tsx +++ b/src/components/communities/CommunitySortDropdown.tsx @@ -9,11 +9,11 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { SortDropdown } from '@/components/ui/sort-dropdown'; const SORT_OPTIONS = [ - { value: 'members', label: '👥 Most Members' }, - { value: 'activity', label: '🔥 Most Active' }, - { value: 'cois', label: '🏆 Highest CoIS' }, - { value: 'name', label: '🔤 Name (A-Z)' }, - { value: 'recent', label: '📅 Recently Active' }, + { value: 'members', label: 'Most Members' }, + { value: 'activity', label: 'Most Active' }, + { value: 'cois', label: 'Highest CoIS' }, + { value: 'name', label: 'Name (A-Z)' }, + { value: 'recent', label: 'Recently Active' }, ]; export function CommunitySortDropdown() { diff --git a/src/components/communities/CommunityStats.tsx b/src/components/communities/CommunityStats.tsx index edbd0c8..db2a27f 100644 --- a/src/components/communities/CommunityStats.tsx +++ b/src/components/communities/CommunityStats.tsx @@ -6,7 +6,9 @@ 'use client'; import useSWR from 'swr'; -import { RFDS } from '@/components/rfds'; +import { Activity, Globe, MapPin, Users } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import { RowList } from '@/components/panels/panel'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); @@ -15,11 +17,11 @@ export function CommunityStats() { if (isLoading || !data) { return ( -
+
{[1, 2, 3, 4].map((i) => ( -
-
-
+
+
+
))}
@@ -28,9 +30,9 @@ export function CommunityStats() { if (error || !data.success) { return ( -
+

Failed to load stats -

+

); } @@ -41,37 +43,40 @@ export function CommunityStats() { const showActiveStat = activePercentage >= 0.75; return ( -
- + - - {showActiveStat && ( - )} -
+ ); } -function StatCard({ number, label }: { number: string; label: string }) { +function StatRow({ icon: Icon, number, label }: { icon: LucideIcon; number: string; label: string }) { return ( - +
+
); } diff --git a/src/components/home/become-contributor.tsx b/src/components/home/become-contributor.tsx index 3ab5988..aec35e8 100644 --- a/src/components/home/become-contributor.tsx +++ b/src/components/home/become-contributor.tsx @@ -1,9 +1,10 @@ 'use client'; import React from "react"; -import { RFDS } from "@/components/rfds"; import Link from "next/link"; +import { Panel, PanelEyebrow, PanelSub } from "@/components/panels/panel"; + type Action = { href: string; label: string; external?: boolean }; const contributorData: { @@ -131,6 +132,9 @@ const contributorData: { }, ]; +const ACTION_LINK_CLASS = + "panels-anim text-[15px] font-semibold hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[#16181D]"; + export function BecomeContributor() { const handleContactClick = () => { const parts = ['hello', 'react', 'foundation']; @@ -138,34 +142,31 @@ export function BecomeContributor() { }; return ( -
-
-
-

- Become a Contributor -

-

- Join the movement to sustain and grow the React ecosystem. Contribute code, - organize communities, create educational content, or support financially — - every pathway helps build a stronger ecosystem. -

-
+ + Become a contributor + + Contribute code, organize communities, create educational content, or support + financially. Every pathway helps sustain and grow the React ecosystem. + -
+
{contributorData.map((item) => ( - {item.icon}} - title={item.title} - description={item.description} - actions={ - <> + className="grid grid-cols-[24px_minmax(0,1fr)] items-start gap-x-5 py-6" + > + +
+

{item.title}

+

+ {item.description} +

+
{item.primaryAction.label} @@ -173,29 +174,29 @@ export function BecomeContributor() { {item.secondaryAction && ( {item.secondaryAction.label} )} - - } - /> +
+
+
))}
-
-

+

+

Questions about contributing?{" "}

-
+ ); } diff --git a/src/components/home/ecosystem-libraries.tsx b/src/components/home/ecosystem-libraries.tsx index 0eb756a..98d90d4 100644 --- a/src/components/home/ecosystem-libraries.tsx +++ b/src/components/home/ecosystem-libraries.tsx @@ -1,5 +1,4 @@ -import { ButtonLink } from "@/components/ui/button"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; +import { Panel, PanelEyebrow, PanelSub } from "@/components/panels/panel"; import { ecosystemLibraries } from "@/lib/maintainer-tiers"; import { LibraryCard } from "@/components/ui/library-card"; import { libraryDisplayNames } from "@/lib/library-icons"; @@ -16,7 +15,7 @@ interface EcosystemLibrariesProps { export function EcosystemLibraries({ id = "libraries", - title = "Supported Ecosystem", + title = "Supported ecosystem", description, risScores, showRIS = false, @@ -26,7 +25,7 @@ export function EcosystemLibraries({ const risScoreMap = risScores ? new Map(risScores.map((score) => [score.repo, score.ris])) : new Map(); - + // Use dynamic count from ecosystemLibraries if description not provided const libraryCount = ecosystemLibraries.length; const defaultDescription = `We track contributions across all ${libraryCount} critical React ecosystem libraries:`; @@ -99,60 +98,58 @@ owner/repo: ]; return ( - -
-

{title}

-

{displayDescription}

+ + {title} + {displayDescription} -
- {categorizedLibraries.map((cat) => { - const libs = ecosystemLibraries.filter((l) => l.category === cat.category); - if (libs.length === 0) return null; +
+ {categorizedLibraries.map((cat) => { + const libs = ecosystemLibraries.filter((l) => l.category === cat.category); + if (libs.length === 0) return null; - return ( -
-

- {cat.name} · {libs.length} {libs.length === 1 ? "library" : "libraries"} -

-
- {libs.map((lib, idx) => ( - - ))} -
+ return ( +
+

+ {cat.name} · {libs.length} {libs.length === 1 ? "library" : "libraries"} +

+
+ {libs.map((lib, idx) => ( + + ))}
- ); - })} -
+
+ ); + })} +
-
-

- Total: {ecosystemLibraries.length} libraries tracked · All contributions - verified via GitHub +

+ Total: {ecosystemLibraries.length} libraries tracked · All contributions + verified via GitHub +

+ + {showMissingLibraryIssue ? ( +
+

+ Don't see a library?

+ + Add a missing library +
- - {showMissingLibraryIssue ? ( -
-

- Don't see a library? -

- - Add a missing library - -
- ) : null} -
-
+ ) : null} + ); } diff --git a/src/components/home/executive-message.tsx b/src/components/home/executive-message.tsx index 37aca45..97a071d 100644 --- a/src/components/home/executive-message.tsx +++ b/src/components/home/executive-message.tsx @@ -1,110 +1,102 @@ -import { ScrollReveal } from "@/components/ui/scroll-reveal"; import Image from "next/image"; +import { Panel, PanelEyebrow } from "@/components/panels/panel"; + export function ExecutiveMessage() { return ( - -
-
-
-
- Seth Webster, Executive Director -
-
-

- A Message from Our Executive Director -

-
- -
-

- You know, every so often, something comes along in software that changes not - just how we build — but why we build. -

- -

React did that.

- -

- It gave us more than a way to render UIs. It gave us a new way to think — - about composition, about state, about expressing ideas. But even more than - that, it gave us a new way to connect with one another. -

- -

- From the very beginning, React has been about people. About curiosity shared - in the open. About mentorship that crosses companies, countries, and time - zones. About a community that believes — deeply — that if we help others - bring their ideas to life, ours will follow. -

- -

- That belief has powered one of the most influential movements in modern - software history. React has shaped how the web is built, how mobile is built, - even how we as developers think about creativity itself. And yet, for all its - reach and impact, its heart has never changed: it's still about people - building together. -

- -

That's why we created the React Foundation.

- -

- The Foundation exists to make sure React's future is independent, - community-driven, and open — forever. It's here to protect the culture - that brought us all this way, and to nurture what comes next: the next - generation of maintainers, educators, experimenters, and dreamers. -

- -

- We're doing that by working hand-in-hand with incredible partners — - Meta, Microsoft, Amazon, Vercel, Expo, Callstack, Software Mansion, and many - more — but more importantly, by working with you, the global community of - developers who make React what it is. -

- -

- - Because this isn't just about governance. It's about legacy. -
- It's about ensuring that the ideas we build together endure. -
-

- -

- I've had the privilege of leading React at Meta for many years, and now, - as the Executive Director of the React Foundation, I carry the same North Star - that's guided me all along: helping others bring their ideas to life. -

- -

- If React has ever inspired you — to learn, to teach, to build, to share — - then you are already part of this story. The Foundation is here to help that - story grow, together. -

- -

- Thank you for everything you've built so far — and for everything - you're about to. -

- -

- Let's make the next chapter one that lasts for generations. -

-
- -
-
-

Seth Webster

-

Executive Director, React Foundation

-
-
-
-
+ + A message from our executive director + +
+ Seth Webster, Executive Director +
+ +

+ You know, every so often, something comes along in software that changes not + just how we build — but why we build. +

+ +
+

React did that.

+ +

+ It gave us more than a way to render UIs. It gave us a new way to think — + about composition, about state, about expressing ideas. But even more than + that, it gave us a new way to connect with one another. +

+ +

+ From the very beginning, React has been about people. About curiosity shared + in the open. About mentorship that crosses companies, countries, and time + zones. About a community that believes — deeply — that if we help others + bring their ideas to life, ours will follow. +

+ +

+ That belief has powered one of the most influential movements in modern + software history. React has shaped how the web is built, how mobile is built, + even how we as developers think about creativity itself. And yet, for all its + reach and impact, its heart has never changed: it's still about people + building together. +

+ +

That's why we created the React Foundation.

+ +

+ The Foundation exists to make sure React's future is independent, + community-driven, and open — forever. It's here to protect the culture + that brought us all this way, and to nurture what comes next: the next + generation of maintainers, educators, experimenters, and dreamers. +

+ +

+ We're doing that by working hand-in-hand with incredible partners — + Meta, Microsoft, Amazon, Vercel, Expo, Callstack, Software Mansion, and many + more — but more importantly, by working with you, the global community of + developers who make React what it is. +

+ +

+ + Because this isn't just about governance. It's about legacy. +
+ It's about ensuring that the ideas we build together endure. +
+

+ +

+ I've had the privilege of leading React at Meta for many years, and now, + as the Executive Director of the React Foundation, I carry the same North Star + that's guided me all along: helping others bring their ideas to life. +

+ +

+ If React has ever inspired you — to learn, to teach, to build, to share — + then you are already part of this story. The Foundation is here to help that + story grow, together. +

+ +

+ Thank you for everything you've built so far — and for everything + you're about to. +

+ +

+ Let's make the next chapter one that lasts for generations. +

+
+ +
+

Seth Webster

+

Executive Director, React Foundation

+
+
); } diff --git a/src/components/home/founding-members.tsx b/src/components/home/founding-members.tsx index 3d906cf..d5bacad 100644 --- a/src/components/home/founding-members.tsx +++ b/src/components/home/founding-members.tsx @@ -1,166 +1,50 @@ -import Image from "next/image"; -import { ButtonLink } from "@/components/ui/button"; -import { ScrollReveal } from "@/components/ui/scroll-reveal"; +import { Panel, PanelButton, PanelEyebrow, PanelSub, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; -type ImageMember = { - name: string; - src: string; - filter?: string; - /** Normalized max-width to balance visual weight across logos */ - maxWidth?: number; - /** Override container height (px). Defaults to 40 (h-10). */ - height?: number; -}; - -type TextMember = { - name: string; - text: string; - /** Font size class to balance visual weight with image logos */ - textSize?: string; -}; - -type Member = ImageMember | TextMember; - -function isTextMember(m: Member): m is TextMember { - return "text" in m; -} - -const WHITE_FILTER = "brightness(0) invert(1)"; - -const FOUNDING_MEMBERS: Member[] = [ - { - name: "Meta", - src: "/assets/founding-members/meta.svg", - filter: WHITE_FILTER, - maxWidth: 90, - }, - { - name: "Amazon Developer", - src: "/assets/founding-members/amazon.svg", - filter: WHITE_FILTER, - maxWidth: 89, - }, - { - name: "Microsoft", - src: "/assets/founding-members/microsoft.svg", - filter: WHITE_FILTER, - maxWidth: 132, - height: 58, - }, - { - name: "Huawei", - src: "/assets/founding-members/huawei.svg", - filter: WHITE_FILTER, - maxWidth: 90, - }, - { - name: "Software Mansion", - src: "/assets/founding-members/software-mansion.svg", - maxWidth: 120, - }, - { - name: "Expo", - src: "/assets/founding-members/expo.svg", - filter: WHITE_FILTER, - maxWidth: 90, - }, - { - name: "Callstack", - src: "/assets/founding-members/callstack.svg", - maxWidth: 120, - }, - { - name: "Vercel", - src: "/assets/founding-members/vercel.svg", - filter: WHITE_FILTER, - maxWidth: 90, - }, +const MEMBER_COLUMNS: string[][] = [ + ["Meta", "Amazon Developer", "Microsoft", "Huawei"], + ["Software Mansion", "Expo", "Callstack", "Vercel"], ]; export function FoundingMembers() { return ( - -
-
-

- Founding Members -

-

- We're grateful to our founding members who believe in - sustaining the React ecosystem and supporting open source - maintainers. -

-
- -
- {/* Background */} -
-
- - {/* Logo grid */} -
- {FOUNDING_MEMBERS.map((member) => ( -
- {isTextMember(member) ? ( - - {member.text} - - ) : ( -
- {`${member.name} -
- )} -
+ + Founding members + + We're grateful to our founding members who believe in sustaining the React + ecosystem and supporting open source maintainers. + + +
+ {MEMBER_COLUMNS.map((column, columnIndex) => ( + 0 ? "max-md:border-t max-md:border-[color:var(--panel-rule)]" : undefined + } + > + {column.map((name) => ( + + {name} + + + + ))} -
-
- -
-
-
-

- Become a member -

-

- Help fund React maintainers, education, and ecosystem support. -

-
- - Become a member - -
+ + ))} +
+ +
+
+

Become a member

+

+ Help fund React maintainers, education, and ecosystem support. +

-
-
+ + Become a member + +
+ ); } diff --git a/src/components/home/panels/panel-contribute.tsx b/src/components/home/panels/panel-contribute.tsx new file mode 100644 index 0000000..a1cc935 --- /dev/null +++ b/src/components/home/panels/panel-contribute.tsx @@ -0,0 +1,67 @@ +import { CircleDollarSign, Code, Heart, UserPlus } from "lucide-react"; + +import { Panel, PanelEyebrow, PanelSub, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; +import type { ContentRow } from "@/components/panels/types"; + +const CONTRIBUTE_PATHWAYS: ContentRow[] = [ + { + icon: Code, + title: "Contribute to Repos", + description: + "Submit code, RFCs, documentation, or bug reports to React and 54+ ecosystem libraries.", + cta: { label: "Browse React Repos", href: "https://github.com/facebook/react" }, + }, + { + icon: CircleDollarSign, + title: "Support Financially", + description: + "Store purchases, donations, and sponsorships all go toward maintainers, education, and accessibility work.", + cta: { label: "Learn More", href: "/store" }, + }, + { + icon: Heart, + title: "Sponsor a Library", + description: + "Put money behind a specific library. Choose from 54, including Redux, TanStack Query, and React Router.", + cta: { label: "Browse Libraries", href: "/impact#libraries" }, + }, + { + icon: UserPlus, + title: "Become a Member", + description: + "Members get a vote on funding decisions, updates before anyone else, and recognition in the community.", + cta: { + label: "Apply Now", + href: "https://enrollment.lfx.linuxfoundation.org/?project=react-foundation", + external: true, + }, + }, +]; + +export function PanelContribute() { + return ( + + Become a contributor + + Write code, give money, sponsor a library you depend on, or join as a member. + Every path funds the same work. + + + {CONTRIBUTE_PATHWAYS.map((pathway) => ( + + + ))} + + + ); +} diff --git a/src/components/home/panels/panel-hero.tsx b/src/components/home/panels/panel-hero.tsx new file mode 100644 index 0000000..d3fd62b --- /dev/null +++ b/src/components/home/panels/panel-hero.tsx @@ -0,0 +1,31 @@ +import { OrbitMarks, Panel, PanelActions, PanelButton, PanelPlainLink } from "@/components/panels/panel"; + +export function PanelHero() { + return ( + + +
+

+ Building the future of React, together. +

+

+ The React Foundation funds the work that keeps React healthy: maintenance, + documentation, teaching, and the communities around it. Thousands of + contributors already pitch in. Join them. +

+ + + Get Involved + + + Learn Our Story + + Shop to Support + +
+
+ ); +} diff --git a/src/components/home/panels/panel-join.tsx b/src/components/home/panels/panel-join.tsx new file mode 100644 index 0000000..eb20264 --- /dev/null +++ b/src/components/home/panels/panel-join.tsx @@ -0,0 +1,25 @@ +import { OrbitMarks, Panel, PanelActions, PanelButton, PanelEyebrow, PanelPlainLink } from "@/components/panels/panel"; + +export function PanelJoin() { + return ( + + +
+ Get started +

+ Write code, teach, organize, or give. However you contribute, React stays + maintained and free for everyone. +

+ + + Get Involved + + + View Impact Reports + + Shop the Store + +
+
+ ); +} diff --git a/src/components/home/panels/panel-members.tsx b/src/components/home/panels/panel-members.tsx new file mode 100644 index 0000000..8e5739f --- /dev/null +++ b/src/components/home/panels/panel-members.tsx @@ -0,0 +1,50 @@ +import { Panel, PanelButton, PanelEyebrow, PanelSub, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; +import type { MemberName } from "@/components/panels/types"; + +const MEMBER_COLUMNS: MemberName[][] = [ + ["Meta", "Amazon", "Microsoft", "Huawei"], + ["Software Mansion", "Expo", "Callstack", "Vercel"], +]; + +export function PanelMembers() { + return ( + + Founding members + + Eight companies funded the foundation from day one. We're grateful to every + one of them. + +
+ {MEMBER_COLUMNS.map((column, columnIndex) => ( + 0 ? "max-md:border-t max-md:border-[color:var(--panel-rule)]" : undefined + } + > + {column.map((name) => ( + + {name} + + + + + ))} + + ))} +
+
+
+

Membership

+

+ Help fund React maintainers, education, and accessibility work. +

+
+ + Become a member + +
+
+ ); +} diff --git a/src/components/home/panels/panel-metrics.tsx b/src/components/home/panels/panel-metrics.tsx new file mode 100644 index 0000000..2346f3a --- /dev/null +++ b/src/components/home/panels/panel-metrics.tsx @@ -0,0 +1,31 @@ +import { Globe, Layers, ShieldCheck, Users } from "lucide-react"; + +import { Panel, PanelEyebrow, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; +import type { MetricRow } from "@/components/panels/types"; + +const METRICS: MetricRow[] = [ + { icon: Layers, label: "Ecosystem libraries", value: "54+", href: "/libraries" }, + { icon: Users, label: "Founding members", value: "8", href: "#members" }, + { icon: Globe, label: "Developers served", value: "Millions", href: "/impact" }, + { icon: ShieldCheck, label: "Transparent funding", value: "100%", href: "/impact" }, +]; + +export function PanelMetrics() { + return ( + + By the numbers + + {METRICS.map((metric) => ( + + + ))} + + + ); +} diff --git a/src/components/home/panels/panel-mission.tsx b/src/components/home/panels/panel-mission.tsx new file mode 100644 index 0000000..fff2747 --- /dev/null +++ b/src/components/home/panels/panel-mission.tsx @@ -0,0 +1,15 @@ +import { Panel, PanelEyebrow } from "@/components/panels/panel"; + +export function PanelMission() { + return ( + + Our mission +

+ React now belongs to its community, not any single company. The foundation + exists to make that permanent: funded + maintainers, solid documentation, and places where the next generation of React + developers learns. +

+
+ ); +} diff --git a/src/components/home/panels/panel-pillars.tsx b/src/components/home/panels/panel-pillars.tsx new file mode 100644 index 0000000..a856055 --- /dev/null +++ b/src/components/home/panels/panel-pillars.tsx @@ -0,0 +1,53 @@ +import { BookOpen, Globe, HandHeart } from "lucide-react"; + +import { Panel, PanelEyebrow, PanelSub, Row, RowArrow, RowList, RowRight } from "@/components/panels/panel"; +import type { ContentRow } from "@/components/panels/types"; + +const PILLARS: ContentRow[] = [ + { + icon: HandHeart, + title: "Fund Maintainers", + description: + "Direct financial support for the developers who maintain the libraries you depend on every day.", + cta: { label: "See Impact", href: "/impact" }, + }, + { + icon: BookOpen, + title: "Education & Resources", + description: + "Tutorials, documentation, workshops, and learning materials that help developers master React.", + cta: { label: "Learn More", href: "/impact" }, + }, + { + icon: Globe, + title: "Global Accessibility", + description: + "React stays free and learnable for developers everywhere, regardless of location, background, or resources.", + cta: { label: "Our Commitment", href: "/impact" }, + }, +]; + +export function PanelPillars() { + return ( + + What we fund + Every contribution lands in one of three programs. + + {PILLARS.map((pillar) => ( + + + ))} + + + ); +} diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 6162f5e..ce0ff57 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -18,6 +18,23 @@ export function Header() { const [isAdmin, setIsAdmin] = useState(false); const isStorePage = pathname?.startsWith("/store"); const isComingSoonPage = pathname === "/coming-soon"; + // Redesigned foundation routes render the header as a floating panel; store, + // admin, profile, and coming-soon keep the classic bar until their own pass. + const PANELS_ROUTE_PREFIXES = [ + "/about", + "/impact", + "/libraries", + "/updates", + "/authors", + "/communities", + "/become-a-member", + "/privacy", + "/terms", + "/auth", + ]; + const isPanelsRoute = + pathname === "/" || + PANELS_ROUTE_PREFIXES.some((prefix) => pathname?.startsWith(prefix)); // Check admin status when user session changes useEffect(() => { @@ -45,33 +62,61 @@ export function Header() { }, [session?.user?.email]); return ( -
-
+
+
{/* Logo */} -
-
- + {isPanelsRoute ? ( + +
React Foundation logo - -
-
- -

- React Foundation -

-

- {isStorePage ? "Official Store" : "Supporting the Ecosystem"} -

- +
+

React Foundation

+ + ) : ( +
+
+ + React Foundation logo + +
+
+ +

+ React Foundation +

+

+ {isStorePage ? "Official Store" : "Supporting the Ecosystem"} +

+ +
-
+ )} {/* Desktop Navigation (hidden on mobile) */}
@@ -79,29 +124,29 @@ export function Header() { {isStorePage ? ( // Store navigation <> - + Collections - + Limited Drops - + Impact ) : ( // Foundation navigation <> - + About - + Updates - + Impact - + Communities {isAdmin && ( @@ -150,6 +195,14 @@ export function Header() { href="/profile" className="transition hover:border-primary/50 hover:shadow-lg hover:shadow-primary/20" /> + ) : isPanelsRoute ? ( + // Ink button per the panels language; `!` outranks the unlayered `a.inline-flex { color: currentColor }` rule. + + Sign in + ) : ( Sign in diff --git a/src/components/panels/panel.tsx b/src/components/panels/panel.tsx new file mode 100644 index 0000000..1a14398 --- /dev/null +++ b/src/components/panels/panel.tsx @@ -0,0 +1,236 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; + +import { cn } from "@/lib/cn"; +import type { PanelTone } from "./types"; + +/* + * Panel surfaces keep constant colors in both themes; only the page gutter is + * theme-aware. Tone-specific row/eyebrow colors flow down as CSS variables so + * the row primitives stay tone-agnostic, mirroring the prototype's CSS. + */ +const TONE_CLASSES: Record = { + cyan: cn( + "bg-[#58C4DC]", + "[--panel-rule:rgba(22,24,29,0.2)]", + "[--panel-hover:rgba(255,255,255,0.25)]", + "[--panel-eyebrow:rgba(22,24,29,0.65)]", + "[--panel-sub:rgba(22,24,29,0.7)]", + ), + paper: cn( + "bg-[#F6F7F9]", + "[--panel-rule:#16181D]", + "[--panel-hover:#FFFFFF]", + "[--panel-eyebrow:#5E687E]", + "[--panel-sub:#5E687E]", + ), +}; + +const FOCUS_RING = cn( + "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[#16181D]", +); + +export function Panel({ + tone, + id, + labelledBy, + compact = false, + className, + children, +}: { + tone: PanelTone; + id?: string; + labelledBy: string; + compact?: boolean; + className?: string; + children: ReactNode; +}) { + return ( +
+ {children} +
+ ); +} + +export function PanelEyebrow({ + id, + as: Tag = "h2", + children, +}: { + id?: string; + as?: "h2" | "p"; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function PanelSub({ children }: { children: ReactNode }) { + return

{children}

; +} + +export function PanelActions({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function PanelButton({ + href, + variant, + children, +}: { + href: string; + variant: "ink" | "outline"; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function PanelPlainLink({ href, children }: { href: string; children: ReactNode }) { + return ( + + {children} + + ); +} + +export function RowList({ className, children }: { className?: string; children: ReactNode }) { + return
{children}
; +} + +export function Row({ + href, + external = false, + bare = false, + className, + children, +}: { + href: string; + external?: boolean; + /** Two-column row without the icon column (founding member rows). */ + bare?: boolean; + className?: string; + children: ReactNode; +}) { + const rowClassName = cn( + // Inset content from the row edges, with a matching negative margin so + // resting text keeps the panel's alignment and only the hover surface grows. + "group panels-anim -mx-4 grid items-center gap-x-5 px-4 text-[#16181D] hover:bg-[var(--panel-hover)]", + FOCUS_RING, + bare + ? "grid-cols-[minmax(0,1fr)_auto]" + : "grid-cols-[24px_1fr] gap-y-2 md:grid-cols-[24px_minmax(0,1fr)_auto] md:gap-y-0", + className, + ); + + if (href.startsWith("http")) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +export function RowRight({ + bare = false, + className, + children, +}: { + bare?: boolean; + className?: string; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function RowArrow() { + return ( + + ); +} + +const ORBIT_SCALES = [12.7, 20.9, 29.1, 37.2]; +const ORBIT_ROTATIONS = [0, 60, 120]; + +/* + * Concentric React-orbit line art. vector-effect="non-scaling-stroke" keeps + * every ring at a true 1px stroke no matter how far the group is scaled up. + */ +export function OrbitMarks({ className }: { className?: string }) { + return ( + + ); +} diff --git a/src/components/panels/panels-footer.tsx b/src/components/panels/panels-footer.tsx new file mode 100644 index 0000000..b509bb0 --- /dev/null +++ b/src/components/panels/panels-footer.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; + +import { cn } from "@/lib/cn"; + +// The `!` marks are load-bearing: globals.css has unlayered `a:not(.inline-flex) { color: inherit }` +// which otherwise beats layered text utilities on links. +const FOOTER_LINK_CLASS = cn( + "text-[13px] text-[#087EA4]! dark:text-[#58C4DC]!", + "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid", + "focus-visible:outline-[#087EA4] dark:focus-visible:outline-[#58C4DC]", +); + +export function PanelsFooter() { + return ( + // pt-8 plus the page's panel gap adds up to the prototype's 48px footer offset. +
+ +

+ Copyright © The Linux Foundation®. All rights reserved. The Linux Foundation has + registered trademarks and uses trademarks. For more information, including terms + of use, privacy policy, and trademark usage, please see our{" "} + + Policies page + + . +

+ +
+ ); +} diff --git a/src/components/panels/types.ts b/src/components/panels/types.ts new file mode 100644 index 0000000..4833071 --- /dev/null +++ b/src/components/panels/types.ts @@ -0,0 +1,25 @@ +import type { LucideIcon } from "lucide-react"; + +export type PanelTone = "cyan" | "paper"; + +export type Cta = { + label: string; + href: string; + external?: boolean; +}; + +export type MetricRow = { + icon: LucideIcon; + label: string; + value: string; + href: string; +}; + +export type ContentRow = { + icon: LucideIcon; + title: string; + description: string; + cta: Cta; +}; + +export type MemberName = string; diff --git a/src/components/rfds/TableOfContents.tsx b/src/components/rfds/TableOfContents.tsx index ca14738..3b7f396 100644 --- a/src/components/rfds/TableOfContents.tsx +++ b/src/components/rfds/TableOfContents.tsx @@ -13,8 +13,8 @@ 'use client'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import React, { useEffect, useMemo, useState } from 'react'; +import { motion, AnimatePresence, MotionConfig } from 'framer-motion'; import { ListTree, ChevronRight, Search, PanelRightOpen, PanelRightClose, ArrowUp } from 'lucide-react'; export interface TOCSection { @@ -27,6 +27,12 @@ interface TableOfContentsProps { sections: TOCSection[]; title?: string; className?: string; + /** + * "panels" matches the redesigned foundation pages: flat theme-aware chrome, + * no glass, gradients, or glow. "default" keeps the original look for the + * routes that still use it (scoring, theme-test). + */ + variant?: 'default' | 'panels'; } /** Utility: class name merger */ @@ -78,6 +84,7 @@ function TOCInner({ onItemClick, setQuery, query, + panels = false, }: { title: string; headings: TOCSection[]; @@ -85,38 +92,63 @@ function TOCInner({ onItemClick: (id: string) => void; setQuery?: (q: string) => void; query?: string; + panels?: boolean; }) { return (
{/* Header */}
-
- -
-
+ {!panels && ( +
+ +
+
+ )}
-

{title}

-

Jump to any section

+

+ {title} +

+ {!panels &&

Jump to any section

}
{/* Search */} {setQuery && ( )} {/* List */}