Improve HK sidebar and restore nested content scrolling
This commit is contained in:
1 parent
bf126fd825
commit
2578bf6a09
27 files changed
+184
-20
No files matched your search
@@ -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<Record<string, boolean>>({});
|
||||
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")}
|
||||
>
|
||||
<div className="shrink-0 px-3 pt-3">
|
||||
<div className="relative">
|
||||
<Search
|
||||
size={16}
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[var(--admin-sidebar-muted-readable)]"
|
||||
/>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => 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 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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)]"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1 border-b border-[var(--admin-border)] px-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={expandAll}
|
||||
disabled={allExpanded}
|
||||
disabled={allExpanded || !!search}
|
||||
title={t("expandAll")}
|
||||
aria-label={t("expandAll")}
|
||||
className={cn(
|
||||
@@ -186,7 +259,7 @@ export function AdminSidebarNav({
|
||||
<button
|
||||
type="button"
|
||||
onClick={collapseAll}
|
||||
disabled={!canCollapseMore}
|
||||
disabled={!canCollapseMore || !!search}
|
||||
title={t("collapseAll")}
|
||||
aria-label={t("collapseAll")}
|
||||
className={cn(
|
||||
@@ -205,9 +278,17 @@ export function AdminSidebarNav({
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 py-3">
|
||||
{groups.map((group, gi) => {
|
||||
{visibleGroups.length === 0 && (
|
||||
<p
|
||||
role="status"
|
||||
className="px-2 py-6 text-sm text-[var(--admin-sidebar-muted-readable)]"
|
||||
>
|
||||
{t("noMenuResults")}
|
||||
</p>
|
||||
)}
|
||||
{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({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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({
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{t(group.labelKey)}
|
||||
</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className="text-xs tabular-nums text-[var(--admin-sidebar-muted-readable)]"
|
||||
>
|
||||
{group.items.length}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={cn(
|
||||
|
||||
Reference in new issue
Block a user