fix(site): make the docs and landing navbars identical (#25)
Co-authored-by: Alice <alice@prismshadow.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
6 files changed
+64
-41
No files matched your search
@@ -1,10 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Sticky top bar, landing-parity: logo + site name + "Docs" badge, then the SAME
|
* Sticky top bar, landing-parity: logo + site name, then the SAME link row as the
|
||||||
* link row as the landing page's nav (section anchors on the landing home, blog,
|
* landing page's nav (section anchors on the landing home, blog, docs) with the same
|
||||||
* docs) with the same sliding hover pill — so the two sites link into each other
|
* sliding hover pill — so the two sites render an identical navbar. Section/blog links
|
||||||
* seamlessly. Section/blog links are plain anchors into the landing SPA one level
|
* are plain anchors into the landing SPA one level up; "Docs" routes to this site's
|
||||||
* up; "Docs" routes to this site's own root. The right side keeps the language
|
* own root and is always the active link. The right cluster also matches the landing
|
||||||
* and theme toggles, GitHub, and — on small screens — the sidebar toggle.
|
* nav (language + theme toggles, GitHub) and ends with the sidebar toggle on <lg
|
||||||
|
* screens — the same slot where the landing nav keeps its mobile-menu button on <md.
|
||||||
|
*
|
||||||
|
* The markup and class strings are duplicated verbatim from the landing nav
|
||||||
|
* (packages/landing/src/components/nav.tsx — the two sites share no package, as with
|
||||||
|
* site-prefs.ts); keep the two files aligned so the navbars render identically.
|
||||||
*/
|
*/
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import type { MouseEvent } from "react";
|
import type { MouseEvent } from "react";
|
||||||
@@ -29,11 +34,13 @@ export function Nav({ menuOpen, onToggleMenu }: { menuOpen: boolean; onToggleMen
|
|||||||
features: S.nav.features,
|
features: S.nav.features,
|
||||||
};
|
};
|
||||||
|
|
||||||
const linkCls =
|
|
||||||
"relative z-10 rounded-md px-2.5 py-1.5 text-sm text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100";
|
|
||||||
// Landing-parity active style: on the docs site, the "Docs" link is the current page.
|
|
||||||
const activeLinkCls =
|
const activeLinkCls =
|
||||||
"relative z-10 rounded-md px-2.5 py-1.5 text-sm transition-colors bg-black text-white hover:bg-black hover:text-white dark:bg-black dark:text-white dark:ring-1 dark:ring-gray-600 dark:hover:bg-black dark:hover:text-white";
|
"bg-black text-white hover:bg-black hover:text-white dark:bg-black dark:text-white dark:ring-1 dark:ring-gray-600 dark:hover:bg-black dark:hover:text-white";
|
||||||
|
const inactiveLinkCls = "text-gray-600 dark:text-gray-400";
|
||||||
|
const deskInactiveLinkCls = `${inactiveLinkCls} hover:text-gray-900 dark:hover:text-gray-100`;
|
||||||
|
// Same class recipe as the landing nav's desktop links; here only "Docs" is ever active.
|
||||||
|
const deskLinkCls = (active: boolean) =>
|
||||||
|
`relative z-10 rounded-md px-2.5 py-1.5 text-sm transition-colors ${active ? activeLinkCls : deskInactiveLinkCls}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The hover pill appears IN PLACE under the first link it lands on (position
|
* The hover pill appears IN PLACE under the first link it lands on (position
|
||||||
@@ -64,25 +71,34 @@ export function Nav({ menuOpen, onToggleMenu }: { menuOpen: boolean; onToggleMen
|
|||||||
pillVisible.current = false;
|
pillVisible.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const desktopLinks = (
|
||||||
|
<>
|
||||||
|
{SECTION_IDS.map((id) => (
|
||||||
|
<a
|
||||||
|
key={id}
|
||||||
|
href={`${SITE_URL}#${id}`}
|
||||||
|
className={deskLinkCls(false)}
|
||||||
|
onMouseEnter={slideTo}
|
||||||
|
>
|
||||||
|
{sectionLabel[id]}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<a href={`${SITE_URL}blog`} className={deskLinkCls(false)} onMouseEnter={slideTo}>
|
||||||
|
{S.nav.blog}
|
||||||
|
</a>
|
||||||
|
{/* "Docs" is this SPA's own root — a router Link, and always the current page. */}
|
||||||
|
<Link to="/" className={deskLinkCls(true)} onMouseEnter={slideTo} aria-current="page">
|
||||||
|
{S.nav.docs}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b border-gray-200 bg-white/85 backdrop-blur dark:border-gray-800 dark:bg-gray-950/85">
|
<header className="sticky top-0 z-40 border-b border-gray-200 bg-white/85 backdrop-blur dark:border-gray-800 dark:bg-gray-950/85">
|
||||||
<div className="mx-auto flex h-14 max-w-7xl items-center gap-2 px-4 sm:px-6">
|
<div className="mx-auto flex h-14 max-w-7xl items-center gap-2 px-4 sm:px-6">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onToggleMenu}
|
|
||||||
aria-label={menuOpen ? S.nav.closeMenu : S.nav.openMenu}
|
|
||||||
aria-expanded={menuOpen}
|
|
||||||
className="mr-1 inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 lg:hidden dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900"
|
|
||||||
>
|
|
||||||
{menuOpen ? <XIcon className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href={SITE_URL} className="flex items-center gap-2">
|
<a href={SITE_URL} className="flex items-center gap-2">
|
||||||
<img src={`${import.meta.env.BASE_URL}penguin-logo.svg`} alt="" className="h-7 w-7" />
|
<img src={`${import.meta.env.BASE_URL}penguin-logo.svg`} alt="" className="h-7 w-7" />
|
||||||
<span className="text-[15px] font-semibold tracking-tight">{S.siteName}</span>
|
<span className="text-[15px] font-semibold tracking-tight">{S.siteName}</span>
|
||||||
<span className="rounded-full border border-brand-200 bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-700 dark:border-brand-900 dark:bg-brand-950 dark:text-brand-300">
|
|
||||||
{S.docsBadge}
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
@@ -90,24 +106,14 @@ export function Nav({ menuOpen, onToggleMenu }: { menuOpen: boolean; onToggleMen
|
|||||||
aria-label="Primary"
|
aria-label="Primary"
|
||||||
onMouseLeave={hidePill}
|
onMouseLeave={hidePill}
|
||||||
>
|
>
|
||||||
{/* Sliding hover pill: appears in place, slides between links, fades out in place. */}
|
{/* Sliding hover pill: appears in place, slides between links, fades out in place; active links retain their own background. */}
|
||||||
<span
|
<span
|
||||||
ref={pillRef}
|
ref={pillRef}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="absolute top-1/2 h-8 -translate-y-1/2 rounded-md bg-gray-100 transition-[left,width,opacity] duration-200 ease-out dark:bg-gray-800"
|
className="absolute top-1/2 h-8 -translate-y-1/2 rounded-md bg-gray-100 transition-[left,width,opacity] duration-200 ease-out dark:bg-gray-800"
|
||||||
style={{ left: 0, width: 0, opacity: 0 }}
|
style={{ left: 0, width: 0, opacity: 0 }}
|
||||||
/>
|
/>
|
||||||
{SECTION_IDS.map((id) => (
|
{desktopLinks}
|
||||||
<a key={id} href={`${SITE_URL}#${id}`} className={linkCls} onMouseEnter={slideTo}>
|
|
||||||
{sectionLabel[id]}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
<a href={`${SITE_URL}blog`} className={linkCls} onMouseEnter={slideTo}>
|
|
||||||
{S.nav.blog}
|
|
||||||
</a>
|
|
||||||
<Link to="/" className={activeLinkCls} onMouseEnter={slideTo} aria-current="page">
|
|
||||||
{S.nav.docs}
|
|
||||||
</Link>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-1">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
@@ -123,6 +129,17 @@ export function Nav({ menuOpen, onToggleMenu }: { menuOpen: boolean; onToggleMen
|
|||||||
>
|
>
|
||||||
<GitHubIcon className="h-[18px] w-[18px]" />
|
<GitHubIcon className="h-[18px] w-[18px]" />
|
||||||
</a>
|
</a>
|
||||||
|
{/* Sidebar toggle: same slot and styling as the landing nav's menu button, but it
|
||||||
|
opens the docs sidebar and hides at lg (the sidebar's own breakpoint). */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggleMenu}
|
||||||
|
aria-label={menuOpen ? S.nav.closeMenu : S.nav.openMenu}
|
||||||
|
aria-expanded={menuOpen}
|
||||||
|
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 lg:hidden dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900"
|
||||||
|
>
|
||||||
|
{menuOpen ? <XIcon className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Strings } from "./strings";
|
|||||||
|
|
||||||
export const en: Strings = {
|
export const en: Strings = {
|
||||||
siteName: "PenguinHarness",
|
siteName: "PenguinHarness",
|
||||||
docsBadge: "Docs",
|
|
||||||
|
|
||||||
nav: {
|
nav: {
|
||||||
highlights: "Highlights",
|
highlights: "Highlights",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
*/
|
*/
|
||||||
export const zh = {
|
export const zh = {
|
||||||
siteName: "PenguinHarness",
|
siteName: "PenguinHarness",
|
||||||
docsBadge: "Docs",
|
|
||||||
|
|
||||||
nav: {
|
nav: {
|
||||||
// Landing-parity labels: the top bar mirrors the landing page's nav exactly
|
// Landing-parity labels: the top bar mirrors the landing page's nav exactly
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
/** Site footer: brand + product/resource link columns + copyright. */
|
/**
|
||||||
|
* Site footer: brand + product/resource link columns + copyright. The container is
|
||||||
|
* max-w-7xl to frame the page like the nav (chrome shared with the docs site), while
|
||||||
|
* content sections stay max-w-6xl.
|
||||||
|
*/
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { S } from "../lib/strings";
|
import { S } from "../lib/strings";
|
||||||
import { DOCS_URL, LICENSE_URL, RELEASES_URL, REPO_URL } from "../lib/links";
|
import { DOCS_URL, LICENSE_URL, RELEASES_URL, REPO_URL } from "../lib/links";
|
||||||
@@ -12,7 +16,7 @@ export function Footer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-gray-200 dark:border-gray-800">
|
<footer className="border-t border-gray-200 dark:border-gray-800">
|
||||||
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-6">
|
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6">
|
||||||
<div className="flex flex-col gap-10 sm:flex-row sm:justify-between">
|
<div className="flex flex-col gap-10 sm:flex-row sm:justify-between">
|
||||||
<div className="max-w-xs">
|
<div className="max-w-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function LangToggle() {
|
|||||||
{open && (
|
{open && (
|
||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
className="anim-pop absolute right-0 z-40 mt-1 w-36 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"
|
className="anim-fade absolute right-0 z-40 mt-1 w-36 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"
|
||||||
>
|
>
|
||||||
{OPTIONS.map((o) => (
|
{OPTIONS.map((o) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
* LIVE scroll position (scroll-spy), so the highlight follows as you scroll; on other
|
* LIVE scroll position (scroll-spy), so the highlight follows as you scroll; on other
|
||||||
* routes it falls back to route state (e.g. Blog). Section links route through "/#id"
|
* routes it falls back to route state (e.g. Blog). Section links route through "/#id"
|
||||||
* so the URL hash stays in sync on click. A disclosure menu covers small screens.
|
* so the URL hash stays in sync on click. A disclosure menu covers small screens.
|
||||||
|
*
|
||||||
|
* The markup and class strings are duplicated verbatim in the docs nav
|
||||||
|
* (packages/docs/src/components/nav.tsx — the two sites share no package, as with
|
||||||
|
* site-prefs.ts); keep the two files aligned so the navbars render identically.
|
||||||
*/
|
*/
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import type { MouseEvent } from "react";
|
import type { MouseEvent } from "react";
|
||||||
@@ -141,7 +145,7 @@ export function Nav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-40 border-b border-gray-200 bg-white/85 backdrop-blur dark:border-gray-800 dark:bg-gray-950/85">
|
<header className="sticky top-0 z-40 border-b border-gray-200 bg-white/85 backdrop-blur dark:border-gray-800 dark:bg-gray-950/85">
|
||||||
<div className="mx-auto flex h-14 max-w-6xl items-center gap-2 px-4 sm:px-6">
|
<div className="mx-auto flex h-14 max-w-7xl items-center gap-2 px-4 sm:px-6">
|
||||||
<Link to="/" className="flex items-center gap-2" onClick={() => setOpen(false)}>
|
<Link to="/" className="flex items-center gap-2" onClick={() => setOpen(false)}>
|
||||||
<img src={`${import.meta.env.BASE_URL}penguin-logo.svg`} alt="" className="h-7 w-7" />
|
<img src={`${import.meta.env.BASE_URL}penguin-logo.svg`} alt="" className="h-7 w-7" />
|
||||||
<span className="text-[15px] font-semibold tracking-tight">{S.siteName}</span>
|
<span className="text-[15px] font-semibold tracking-tight">{S.siteName}</span>
|
||||||
|
|||||||
Reference in new issue
Block a user