Update navigation

This commit is contained in:
Jake Runyan 2026-03-04 11:35:38 -08:00
parent 753112ff46
commit 51845db643
5 changed files with 156 additions and 21 deletions

View File

@ -23,7 +23,9 @@ const App = () => (
<ContentProvider> <ContentProvider>
<> <>
<Navbar /> <Navbar />
<main className="app-content">
<PageRoutes /> <PageRoutes />
</main>
<Footer /> <Footer />
</> </>
</ContentProvider> </ContentProvider>

View File

@ -1,10 +1,10 @@
.navbar { .navbar {
display: grid; position: relative;
grid-template-columns: 1fr auto 1fr; display: flex;
column-gap: 24px;
align-items: center; align-items: center;
justify-content: space-between;
background-color: var(--color-bg-nav); background-color: var(--color-bg-nav);
padding: 10px 20px; padding: 0 20px;
height: var(--nav-height); height: var(--nav-height);
box-sizing: border-box; box-sizing: border-box;
} }
@ -13,6 +13,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
z-index: 1;
} }
.navbar-left .logo { .navbar-left .logo {
@ -23,7 +24,6 @@
.navbar-links { .navbar-links {
display: flex; display: flex;
flex-direction: row;
gap: 20px; gap: 20px;
} }
@ -42,8 +42,13 @@
opacity: 1; opacity: 1;
} }
/* Absolutely positioned so it's always centered relative to the full navbar width */
.navbar-center { .navbar-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
text-align: center; text-align: center;
pointer-events: none;
} }
.navbar-center h1 { .navbar-center h1 {
@ -55,23 +60,107 @@
text-transform: uppercase; text-transform: uppercase;
margin: 0; margin: 0;
white-space: nowrap; white-space: nowrap;
pointer-events: auto;
} }
.navbar-right { .navbar-right {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end;
gap: 16px; gap: 16px;
z-index: 1;
} }
.navbar-right .icon { .navbar-social {
display: none;
}
.icon {
height: 28px; height: 28px;
width: 28px; width: 28px;
opacity: 0.75; opacity: 0.75;
transition: opacity var(--transition-fast), transform var(--transition-fast); transition: opacity var(--transition-fast), transform var(--transition-fast);
} }
.navbar-right .icon:hover { .icon:hover {
opacity: 1; opacity: 1;
transform: scale(1.1); transform: scale(1.1);
} }
.navbar-hamburger {
display: flex;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.navbar-hamburger span {
display: block;
width: 22px;
height: 2px;
background-color: var(--color-text-nav);
border-radius: 2px;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.navbar-hamburger.open span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.navbar-hamburger.open span:nth-child(2) {
opacity: 0;
}
.navbar-hamburger.open span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* Mobile dropdown menu */
.navbar-mobile-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--color-bg-nav);
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 20px 20px;
gap: 16px;
z-index: 100;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.navbar-mobile-menu a {
color: var(--color-text-nav);
text-decoration: none;
font-family: var(--font-heading);
font-size: var(--font-size-base);
letter-spacing: 0.05em;
text-transform: uppercase;
opacity: 0.75;
transition: opacity var(--transition-fast);
}
.navbar-mobile-menu a:hover {
opacity: 1;
}
.mobile-menu-icons {
display: flex;
justify-content: center;
gap: 20px;
padding-top: 4px;
}
.mobile-menu-icons .icon {
height: 22px;
width: 22px;
}
/* Nav links are always in the dropdown, never inline */
.navbar-links {
display: none;
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useSiteConfig } from '../framework/ContentProvider'; import { useSiteConfig } from '../framework/ContentProvider';
import './Navbar.css'; import './Navbar.css';
@ -18,6 +18,7 @@ const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
const Navbar = () => { const Navbar = () => {
const { siteTitle, nav, social } = useSiteConfig(); const { siteTitle, nav, social } = useSiteConfig();
const [isDarkMode, setIsDarkMode] = useState(darkModeQuery.matches); const [isDarkMode, setIsDarkMode] = useState(darkModeQuery.matches);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => { useEffect(() => {
const handler = (e) => setIsDarkMode(e.matches); const handler = (e) => setIsDarkMode(e.matches);
@ -25,6 +26,22 @@ const Navbar = () => {
return () => darkModeQuery.removeEventListener('change', handler); return () => darkModeQuery.removeEventListener('change', handler);
}, []); }, []);
useEffect(() => {
const handleResize = () => { if (window.innerWidth > 640) setMenuOpen(false); };
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const SocialIcons = () => social.map(({ label, url, icon }) => {
const icons = ICON_MAP[icon];
const src = icons ? (isDarkMode ? icons.dark : icons.light) : null;
return (
<a key={icon} href={url} target="_blank" rel="noopener noreferrer">
{src && <img src={src} alt={label} className="icon" title={label} />}
</a>
);
});
return ( return (
<nav className="navbar"> <nav className="navbar">
<div className="navbar-left"> <div className="navbar-left">
@ -37,20 +54,35 @@ const Navbar = () => {
))} ))}
</div> </div>
</div> </div>
<div className="navbar-center"> <div className="navbar-center">
<h1>{siteTitle}</h1> <h1>{siteTitle}</h1>
</div> </div>
<div className="navbar-right"> <div className="navbar-right">
{social.map(({ label, url, icon }) => { <div className="navbar-social">
const icons = ICON_MAP[icon]; <SocialIcons />
const src = icons ? (isDarkMode ? icons.dark : icons.light) : null;
return (
<a key={icon} href={url} target="_blank" rel="noopener noreferrer">
{src && <img src={src} alt={label} className="icon" title={label} />}
</a>
);
})}
</div> </div>
<button
className={`navbar-hamburger${menuOpen ? ' open' : ''}`}
onClick={() => setMenuOpen(o => !o)}
aria-label="Toggle menu"
aria-expanded={menuOpen}
>
<span /><span /><span />
</button>
</div>
{menuOpen && (
<div className="navbar-mobile-menu">
{nav.map(({ label, path }) => (
<Link key={path} to={path} onClick={() => setMenuOpen(false)}>{label}</Link>
))}
<div className="mobile-menu-icons">
<SocialIcons />
</div>
</div>
)}
</nav> </nav>
); );
}; };

View File

@ -7,6 +7,19 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
#root {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-content {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--color-bg);
}
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;

View File

@ -1,6 +1,5 @@
.gallery { .gallery {
display: grid; width: 100%;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin: 0 auto; margin: 0 auto;
background-color: var(--color-bg); background-color: var(--color-bg);
} }