feat(nav): קיבוץ הניווט העליון בתפריטים נפתחים (פסיקה/סגנון) #147

Merged
chaim merged 1 commits from worktree-nav-tidy into main 2026-06-08 07:20:23 +00:00

View File

@@ -1,6 +1,6 @@
"use client";
import type { ReactNode } from "react";
import { Fragment, type ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronDown, Settings } from "lucide-react";
@@ -22,7 +22,7 @@ import { usePendingApprovals } from "@/lib/api/chair";
* Ezer Mishpati navigation shell — two-row header.
*
* Row 1 (brand): logo + dynamic context subtitle · global search · agent boards
* Row 2 (nav): work group · knowledge group · admin dropdown
* Row 2 (nav): work links · knowledge dropdowns (פסיקה/סגנון) · admin dropdown
*
* Editorial/judicial aesthetic preserved:
* - Navy background with a gold hairline rule (border-b-3)
@@ -33,29 +33,39 @@ import { usePendingApprovals } from "@/lib/api/chair";
* sighted users see where they are.
*/
type NavItem = { href: string; label: string };
type NavGroup = { id: string; items: NavItem[] };
// `groupLabel`, when present, renders a separator + sub-heading inside a
// dropdown *before* this item — used to set off secondary tools.
type NavItem = { href: string; label: string; groupLabel?: string };
type NavMenuDef = { id: string; label: string; items: NavItem[] };
const NAV_GROUPS: NavGroup[] = [
// Work cluster — direct links, the daily hubs.
const WORK_LINKS: NavItem[] = [
{ href: "/", label: "בית" },
{ href: "/approvals", label: "מרכז אישורים" },
{ href: "/feedback", label: "הערות יו״ר" },
{ href: "/archive", label: "ארכיון" },
];
// Knowledge cluster — grouped under dropdowns so the bar stays scannable.
// All routes are preserved; only their placement in the nav changes.
const KNOWLEDGE_MENUS: NavMenuDef[] = [
{
id: "work",
id: "precedents",
label: "פסיקה",
items: [
{ href: "/", label: "בית" },
{ href: "/approvals", label: "מרכז אישורים" },
{ href: "/feedback", label: "הערות יו״ר" },
{ href: "/archive", label: "ארכיון" },
{ href: "/precedents", label: "ספריית פסיקה" },
{ href: "/digests", label: "יומונים" },
{ href: "/missing-precedents", label: "פסיקה חסרה" },
{ href: "/graph", label: "מפת הקורפוס", groupLabel: "ניתוח וכיול" },
{ href: "/goldset", label: "מדגם-זהב" },
],
},
{
id: "knowledge",
id: "style",
label: "סגנון",
items: [
{ href: "/precedents", label: "ספריית פסיקה" },
{ href: "/graph", label: פת הקורפוס" },
{ href: "/digests", label: "יומונים" },
{ href: "/missing-precedents", label: "פסיקה חסרה" },
{ href: "/goldset", label: "מדגם-זהב" },
{ href: "/training", label: "אימון סגנון" },
{ href: "/methodology", label: "מתודולוגיה" },
{ href: "/training", label: "אימון סגנון" },
{ href: "/methodology", label: תודולוגיה" },
],
},
];
@@ -162,21 +172,17 @@ export function AppShell({ children }: { children: ReactNode }) {
{/* ─── Row 2 — section nav ─── */}
<div className="flex items-center gap-3 px-10 pt-1 pb-[18px]">
<nav className="flex items-center gap-3" aria-label="ניווט ראשי">
{NAV_GROUPS.map((group, idx) => (
<div key={group.id} className="flex items-center">
{idx > 0 && (
<span
className="mx-2 h-4 w-px bg-parchment/20"
aria-hidden="true"
/>
)}
<div className="flex items-center gap-1">
{group.items.map((item) => (
<NavLink key={item.href} item={item} active={isActive(pathname, item.href)} />
))}
</div>
</div>
))}
<div className="flex items-center gap-1">
{WORK_LINKS.map((item) => (
<NavLink key={item.href} item={item} active={isActive(pathname, item.href)} />
))}
</div>
<span className="mx-2 h-4 w-px bg-parchment/20" aria-hidden="true" />
<div className="flex items-center gap-1">
{KNOWLEDGE_MENUS.map((menu) => (
<NavMenu key={menu.id} menu={menu} pathname={pathname} />
))}
</div>
</nav>
<DropdownMenu>
@@ -249,7 +255,6 @@ function NavLink({ item, active }: { item: NavItem; active: boolean }) {
`}
>
<span>{item.label}</span>
{item.href === "/missing-precedents" ? <MissingPrecedentsBadge /> : null}
{item.href === "/approvals" ? <ApprovalsBadge /> : null}
{active && (
<span
@@ -261,6 +266,69 @@ function NavLink({ item, active }: { item: NavItem; active: boolean }) {
);
}
/* Knowledge-cluster dropdown (פסיקה / סגנון). The trigger mirrors the admin
* dropdown styling and lights up — gold underline included — when any child
* route is active, so the active section reads even while the menu is closed.
* The "פסיקה חסרה" open-count badge surfaces on the trigger too, so the
* pending count stays visible without opening the menu. */
function NavMenu({ menu, pathname }: { menu: NavMenuDef; pathname: string }) {
const menuActive = menu.items.some((i) => isActive(pathname, i.href));
const hasMissingPrecedents = menu.items.some((i) => i.href === "/missing-precedents");
return (
<DropdownMenu>
<DropdownMenuTrigger
className={`
relative flex items-center gap-1 px-3 py-1.5 rounded text-sm
transition-colors outline-none focus-visible:ring-2 focus-visible:ring-gold/60
${menuActive
? "text-parchment font-semibold bg-navy-soft/80"
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"}
data-[state=open]:bg-navy-soft/80 data-[state=open]:text-parchment
`}
aria-label={`${menu.label} — תפריט`}
>
<span>{menu.label}</span>
{hasMissingPrecedents ? <MissingPrecedentsBadge /> : null}
<ChevronDown className="size-3" aria-hidden="true" />
{menuActive && (
<span
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
aria-hidden="true"
/>
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={10} className="min-w-[200px]">
{menu.items.map((item) => {
const active = isActive(pathname, item.href);
return (
<Fragment key={item.href}>
{item.groupLabel && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-muted-foreground">
{item.groupLabel}
</DropdownMenuLabel>
</>
)}
<DropdownMenuItem asChild>
<Link
href={item.href}
aria-current={active ? "page" : undefined}
className={`flex items-center cursor-pointer ${active ? "font-semibold" : ""}`}
>
<span>{item.label}</span>
{item.href === "/missing-precedents" ? <MissingPrecedentsBadge /> : null}
</Link>
</DropdownMenuItem>
</Fragment>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
/* Total pending-approvals badge next to "מרכז אישורים" — Dafna's outstanding
* human-gate items (halachot + missing precedents + feedback + qa_failed).
* Renders only when >0 so the nav stays quiet when everything is cleared. */