Merge pull request 'feat(nav): קיבוץ הניווט העליון בתפריטים נפתחים (פסיקה/סגנון)' (#147) from worktree-nav-tidy into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 58s

This commit was merged in pull request #147.
This commit is contained in:
2026-06-08 07:20:22 +00:00

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import type { ReactNode } from "react"; import { Fragment, type ReactNode } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ChevronDown, Settings } from "lucide-react"; import { ChevronDown, Settings } from "lucide-react";
@@ -22,7 +22,7 @@ import { usePendingApprovals } from "@/lib/api/chair";
* Ezer Mishpati navigation shell — two-row header. * Ezer Mishpati navigation shell — two-row header.
* *
* Row 1 (brand): logo + dynamic context subtitle · global search · agent boards * 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: * Editorial/judicial aesthetic preserved:
* - Navy background with a gold hairline rule (border-b-3) * - 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. * sighted users see where they are.
*/ */
type NavItem = { href: string; label: string }; // `groupLabel`, when present, renders a separator + sub-heading inside a
type NavGroup = { id: string; items: NavItem[] }; // 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: [ items: [
{ href: "/", label: "בית" }, { href: "/precedents", label: "ספריית פסיקה" },
{ href: "/approvals", label: "מרכז אישורים" }, { href: "/digests", label: "יומונים" },
{ href: "/feedback", label: "הערות יו״ר" }, { href: "/missing-precedents", label: "פסיקה חסרה" },
{ href: "/archive", label: "ארכיון" }, { href: "/graph", label: "מפת הקורפוס", groupLabel: "ניתוח וכיול" },
{ href: "/goldset", label: "מדגם-זהב" },
], ],
}, },
{ {
id: "knowledge", id: "style",
label: "סגנון",
items: [ items: [
{ href: "/precedents", label: "ספריית פסיקה" }, { href: "/training", label: "אימון סגנון" },
{ href: "/graph", label: פת הקורפוס" }, { href: "/methodology", label: תודולוגיה" },
{ href: "/digests", label: "יומונים" },
{ href: "/missing-precedents", label: "פסיקה חסרה" },
{ href: "/goldset", label: "מדגם-זהב" },
{ href: "/training", label: "אימון סגנון" },
{ href: "/methodology", label: "מתודולוגיה" },
], ],
}, },
]; ];
@@ -162,21 +172,17 @@ export function AppShell({ children }: { children: ReactNode }) {
{/* ─── Row 2 — section nav ─── */} {/* ─── Row 2 — section nav ─── */}
<div className="flex items-center gap-3 px-10 pt-1 pb-[18px]"> <div className="flex items-center gap-3 px-10 pt-1 pb-[18px]">
<nav className="flex items-center gap-3" aria-label="ניווט ראשי"> <nav className="flex items-center gap-3" aria-label="ניווט ראשי">
{NAV_GROUPS.map((group, idx) => ( <div className="flex items-center gap-1">
<div key={group.id} className="flex items-center"> {WORK_LINKS.map((item) => (
{idx > 0 && ( <NavLink key={item.href} item={item} active={isActive(pathname, item.href)} />
<span ))}
className="mx-2 h-4 w-px bg-parchment/20" </div>
aria-hidden="true" <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) => (
<div className="flex items-center gap-1"> <NavMenu key={menu.id} menu={menu} pathname={pathname} />
{group.items.map((item) => ( ))}
<NavLink key={item.href} item={item} active={isActive(pathname, item.href)} /> </div>
))}
</div>
</div>
))}
</nav> </nav>
<DropdownMenu> <DropdownMenu>
@@ -249,7 +255,6 @@ function NavLink({ item, active }: { item: NavItem; active: boolean }) {
`} `}
> >
<span>{item.label}</span> <span>{item.label}</span>
{item.href === "/missing-precedents" ? <MissingPrecedentsBadge /> : null}
{item.href === "/approvals" ? <ApprovalsBadge /> : null} {item.href === "/approvals" ? <ApprovalsBadge /> : null}
{active && ( {active && (
<span <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 /* Total pending-approvals badge next to "מרכז אישורים" — Dafna's outstanding
* human-gate items (halachot + missing precedents + feedback + qa_failed). * human-gate items (halachot + missing precedents + feedback + qa_failed).
* Renders only when >0 so the nav stays quiet when everything is cleared. */ * Renders only when >0 so the nav stays quiet when everything is cleared. */