+
{/* Mobile header */}
{/* Content */}
-
{children}
+
+ {children}
+
diff --git a/src/components/admin/admin-nav-link.tsx b/src/components/admin/admin-nav-link.tsx
index 2f49b84d..3d7348fd 100644
--- a/src/components/admin/admin-nav-link.tsx
+++ b/src/components/admin/admin-nav-link.tsx
@@ -46,11 +46,12 @@ export function AdminNavLink({
@@ -65,7 +66,7 @@ export function AdminNavLink({
{icon}
) : null}
-
{children}
+
{children}
);
}
diff --git a/src/components/admin/admin-sidebar-nav.tsx b/src/components/admin/admin-sidebar-nav.tsx
index 0fda05e0..1cb5829e 100644
--- a/src/components/admin/admin-sidebar-nav.tsx
+++ b/src/components/admin/admin-sidebar-nav.tsx
@@ -1,6 +1,12 @@
"use client";
-import { ChevronDown, ChevronsDownUp, ChevronsUpDown } from "lucide-react";
+import {
+ ChevronDown,
+ ChevronsDownUp,
+ ChevronsUpDown,
+ Search,
+ X,
+} from "lucide-react";
import { usePathname } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -75,6 +81,7 @@ export function AdminSidebarNav({
const pathname = usePathname() ?? "";
const [collapsed, setCollapsed] = useState
>({});
const [hydrated, setHydrated] = useState(false);
+ const [query, setQuery] = useState("");
const allowed = useMemo(
() => new Set(allowedPermissions),
@@ -94,12 +101,39 @@ export function AdminSidebarNav({
.filter((group) => group.items.length > 0);
}, [allowed, isSuperAdmin, navConfig]);
+ const search = query.trim().toLocaleLowerCase();
+ const visibleGroups = useMemo(
+ () =>
+ groups
+ .map((group) => ({
+ ...group,
+ items: group.items.filter(
+ (item) =>
+ !search ||
+ t(group.labelKey).toLocaleLowerCase().includes(search) ||
+ t(item.labelKey).toLocaleLowerCase().includes(search),
+ ),
+ }))
+ .filter((group) => group.items.length > 0),
+ [groups, search, t],
+ );
+
const allKeys = useMemo(() => groups.map((g) => g.labelKey), [groups]);
useEffect(() => {
- setCollapsed(readCollapsed());
+ const saved = readCollapsed();
+ setCollapsed(
+ Object.fromEntries(
+ groups.map((group) => [
+ group.labelKey,
+ typeof saved[group.labelKey] === "boolean"
+ ? saved[group.labelKey]
+ : true,
+ ]),
+ ),
+ );
setHydrated(true);
- }, []);
+ }, [groups]);
// Keep the group that contains the current page open.
useEffect(() => {
@@ -163,11 +197,50 @@ export function AdminSidebarNav({
className="flex min-h-0 flex-1 flex-col overflow-hidden"
aria-label={t("overview")}
>
+
+
+
+ setQuery(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === "Escape" && query) {
+ event.stopPropagation();
+ setQuery("");
+ }
+ }}
+ aria-label={t("searchMenu")}
+ placeholder={t("searchMenu")}
+ style={{
+ paddingLeft: 36,
+ paddingRight: 36,
+ backgroundColor: "var(--admin-sidebar-background)",
+ color: "var(--admin-sidebar-text-readable)",
+ }}
+ className="h-10 w-full min-w-0 rounded-lg border border-[var(--admin-border)] text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]"
+ />
+ {query && (
+ setQuery("")}
+ aria-label={t("clearMenuSearch")}
+ className="absolute right-1 top-1/2 grid h-8 w-8 -translate-y-1/2 place-items-center rounded-md text-[var(--admin-sidebar-text-readable)] focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]"
+ >
+
+
+ )}
+
+
- {groups.map((group, gi) => {
+ {visibleGroups.length === 0 && (
+
+ {t("noMenuResults")}
+
+ )}
+ {visibleGroups.map((group, gi) => {
const GroupIcon = group.icon;
- const isCollapsed = Boolean(collapsed[group.labelKey]);
+ const isCollapsed = !search && Boolean(collapsed[group.labelKey]);
const panelId = `admin-nav-${group.labelKey}`;
return (
@@ -220,12 +301,13 @@ export function AdminSidebarNav({
toggle(group.labelKey)}
+ disabled={!!search}
aria-expanded={!isCollapsed}
aria-controls={panelId}
className={cn(
- "flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left",
- "text-[0.65rem] font-extrabold uppercase tracking-[0.14em]",
- "text-[var(--admin-sidebar-text-readable)] opacity-70",
+ "flex min-h-10 w-full items-center gap-2 rounded-lg px-2 py-2 text-left",
+ "text-xs font-semibold",
+ "text-[var(--admin-sidebar-muted-readable)]",
"hover:opacity-100 hover:bg-[var(--admin-accent)]/10",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]",
"transition-colors",
@@ -235,6 +317,12 @@ export function AdminSidebarNav({
{t(group.labelKey)}
+
+ {group.items.length}
+
{
@@ -14,7 +17,8 @@ export function SmoothScroll() {
return () => mq.removeEventListener("change", onChange);
}, []);
- if (prefersReduced) return null;
+ // Admin panels own their scroll containers; root wheel smoothing consumes their events.
+ if (prefersReduced || isAdmin) return null;
return (