From 48a539420496965a492d41b08269d0c93093e5b4 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 14 Jul 2026 16:25:36 +0200 Subject: [PATCH] Complete nav rewrite: native details/summary, zero JS state, zero transforms - Mobile nav: uses
/ for toggle, no useState, no useEffect - Nav dropdown: uses
/, no useState, no event listeners - Desktop: separate div with desktop nav items (hidden on mobile) - Mobile: hamburger dropdown with absolute positioned panel - CSS: removed ::after pseudo-element with transform, replaced hover with background-color only, added details[open] CSS rules - No backdrop-filter, no will-change, no transition-all, no GPU hacks - Works on every device without JS state management --- src/app/globals.css | 33 ++++---- src/components/mobile-nav.tsx | 112 +++++++++------------------ src/components/nav-dropdown.tsx | 57 ++++---------- src/components/navigation.tsx | 132 +++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 140 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index fe426f07..b7071dcc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -208,25 +208,14 @@ summary, transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out; border-radius: 0.5rem; } - .nav-item::after { - content: ""; - position: absolute; - bottom: 0; - left: 50%; - width: 60%; - height: 3px; - border-radius: 3px 3px 0 0; - background-color: var(--color-primary); - transform: translateX(-50%) scaleX(0); - transition: transform 0.2s ease-in-out; - } - .nav-item:hover::after { - transform: translateX(-50%) scaleX(1); - } .nav-item:hover { color: var(--color-primary-readable, var(--color-primary)); background-color: color-mix(in srgb, var(--color-primary) 8%, transparent); } + .nav-item[aria-current="page"] { + color: var(--color-primary-readable, var(--color-primary)); + background-color: color-mix(in srgb, var(--color-primary) 10%, transparent); + } .dropdown-item { @apply block py-2.5 px-4 text-sm font-semibold rounded-lg mx-1; color: var(--color-dropdown-text-readable, var(--color-text-readable)); @@ -1076,9 +1065,6 @@ tbody tr:hover { height: auto; min-height: 44px; } - .nav-item::after { - display: none; - } .dropdown-item { font-size: 13px; padding: 0.6rem 0.85rem; @@ -1135,3 +1121,14 @@ body:has([data-admin]) { --color-input: var(--admin-border); --color-ring: var(--admin-accent); } + +/* ── Details/summary open states for nav ──────────────────── */ +details[open] > summary .details-open\:hidden { + display: none; +} +details[open] > summary .details-open\:block { + display: block; +} +details[open] > summary .details-open\:rotate-180 { + transform: rotate(180deg); +} diff --git a/src/components/mobile-nav.tsx b/src/components/mobile-nav.tsx index 5341cc3a..c3f3f46c 100644 --- a/src/components/mobile-nav.tsx +++ b/src/components/mobile-nav.tsx @@ -1,6 +1,6 @@ "use client"; -import { type ReactNode, useEffect, useRef, useState } from "react"; +import { type ReactNode } from "react"; interface MobileNavProps { children: ReactNode; @@ -13,44 +13,14 @@ export function MobileNav({ menuLabel = "Open menu", closeLabel = "Close menu", }: MobileNavProps) { - const [open, setOpen] = useState(false); - const ref = useRef(null); - - useEffect(() => { - if (!open) return; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpen(false); - }; - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [open]); - - useEffect(() => { - if (!open) return; - const onClick = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false); - } - }; - document.addEventListener("mousedown", onClick); - return () => document.removeEventListener("mousedown", onClick); - }, [open]); - return ( -
- - - {open && ( -
setOpen(false)} - style={{ backgroundColor: "rgba(0,0,0,0.6)" }} - /> - )} - - {open && ( -
+ +
+
+ {children}
- )} -
+ +
); } diff --git a/src/components/nav-dropdown.tsx b/src/components/nav-dropdown.tsx index 477793c5..2eaf4bbd 100644 --- a/src/components/nav-dropdown.tsx +++ b/src/components/nav-dropdown.tsx @@ -1,7 +1,5 @@ -"use client"; - import Image from "next/image"; -import { type ReactNode, useEffect, useRef, useState } from "react"; +import { type ReactNode } from "react"; interface NavDropdownProps { label: string; @@ -10,36 +8,10 @@ interface NavDropdownProps { } export function NavDropdown({ label, icon, children }: NavDropdownProps) { - const [open, setOpen] = useState(false); - const ref = useRef(null); - - useEffect(() => { - if (!open) return; - const onClick = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) - setOpen(false); - }; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") setOpen(false); - }; - document.addEventListener("mousedown", onClick); - document.addEventListener("keydown", onKey); - return () => { - document.removeEventListener("mousedown", onClick); - document.removeEventListener("keydown", onKey); - }; - }, [open]); - return ( -
- - {open && ( -
setOpen(false)} - > - {children} -
- )} -
+
+
+ {children} +
+
); } diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 92f4085d..34e14096 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -24,6 +24,7 @@ export async function Navigation() { style={{ backgroundColor: "var(--color-primary, #eeb425)" }} /> + {/* Desktop: home link */}
+ {/* Desktop: nav items */} +
+ + + {t("articles")} + + + {t("photos")} + + + {t("staff")} + + + {t("rankings")} + + + {t("guilds")} + + + + + + {t("shop")} + + + {t("market")} + + + {t("rareValues")} + + + + + + {t("leaderboards")} + + + + + {t("radio")} + + + + + {t("helpCenter")} + + {session?.user ? ( + + {t("myTickets")} + + ) : null} + + {t("badges")} + + + {t("drawBadge")} + + + + {session?.user ? ( + <> + + + {t("friends")} + + + + {t("messages")} + + + ) : null} + + {isStaff ? ( + + + {t("admin")} + + ) : null} +
+ + {/* Mobile: hamburger menu (details/summary) */} - + - + - + + {/* Desktop: controls */}