diff --git a/frontend/src/App.css b/frontend/src/App.css index 6b2add8..4f16779 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -742,6 +742,124 @@ a.app-footer__repo:hover { color: var(--color-error); } +/* ── Hamburger button ─────────────────────────────────────────────────────── */ + +.hamburger-btn { + display: none; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; + border: none; + background: none; + color: var(--color-text-on-header); + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: color 0.15s; +} + +.hamburger-btn:hover { + color: var(--color-text-on-header-hover); +} + +/* ── Mobile nav search inside hamburger menu ──────────────────────────────── */ + +.mobile-nav-search { + display: none; +} + +/* ── Mobile breakpoint (768px) ────────────────────────────────────────────── */ + +@media (max-width: 768px) { + .hamburger-btn { + display: flex; + } + + .app-header { + flex-wrap: wrap; + position: relative; + } + + /* Hide the nav search bar in header on mobile — it goes inside the menu */ + .app-header > .search-container--nav { + display: none; + } + + .app-header__right { + /* Keep the hamburger button visible; nav collapses */ + order: 0; + } + + .app-nav { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + background: var(--color-bg-header); + border-top: 1px solid var(--color-border); + box-shadow: 0 8px 24px var(--color-shadow-heavy); + z-index: 200; + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.25s ease, opacity 0.2s ease; + } + + .app-nav--open { + display: flex; + max-height: 24rem; + opacity: 1; + } + + .app-nav a, + .app-nav .admin-dropdown__trigger { + min-height: 44px; + display: flex; + align-items: center; + padding: 0.75rem 1.5rem; + font-size: 1rem; + border-bottom: 1px solid var(--color-border); + } + + .app-nav .admin-dropdown { + width: 100%; + } + + .app-nav .admin-dropdown__trigger { + width: 100%; + text-align: left; + } + + .app-nav .admin-dropdown__menu { + position: static; + box-shadow: none; + border: none; + border-radius: 0; + background: var(--color-bg-header-alt); + } + + .app-nav .admin-dropdown__item { + padding-left: 2.5rem; + min-height: 44px; + display: flex; + align-items: center; + } + + .mobile-nav-search { + display: block; + padding: 0.75rem 1.5rem; + border-top: 1px solid var(--color-border); + } + + .mobile-nav-search .search-container--nav { + max-width: 100%; + } +} + /* ── Responsive ───────────────────────────────────────────────────────────── */ @media (max-width: 640px) { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cf74b5b..98955df 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect, useRef, useCallback } from "react"; import { Link, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import Home from "./pages/Home"; import SearchResults from "./pages/SearchResults"; @@ -17,10 +18,47 @@ export default function App() { const location = useLocation(); const navigate = useNavigate(); const showNavSearch = location.pathname !== "/"; + const [menuOpen, setMenuOpen] = useState(false); + const headerRef = useRef(null); + + // Close menu on route change + useEffect(() => { + setMenuOpen(false); + }, [location.pathname]); + + // Close menu on Escape + useEffect(() => { + if (!menuOpen) return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setMenuOpen(false); + }; + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [menuOpen]); + + // Close menu on outside click + const handleOutsideClick = useCallback( + (e: MouseEvent) => { + if ( + menuOpen && + headerRef.current && + !headerRef.current.contains(e.target as Node) + ) { + setMenuOpen(false); + } + }, + [menuOpen], + ); + + useEffect(() => { + if (!menuOpen) return; + document.addEventListener("mousedown", handleOutsideClick); + return () => document.removeEventListener("mousedown", handleOutsideClick); + }, [menuOpen, handleOutsideClick]); return (
-
+

Chrysopedia

@@ -33,11 +71,42 @@ export default function App() { /> )}
-