All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 34s
In an RTL paragraph the bidi algorithm puts the *first* logical token on the right, so "פתח דאשבורד Paperclip" rendered visually as "Paperclip" on the LEFT — which reads as the *last* word in Hebrew and looks like an afterthought rather than the brand name the menu opens. Reorders to "Paperclip פתח דאשבורד" so Paperclip sits on the right (read first) and centers the label so it sits above both items instead of hugging the inline-start edge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
253 lines
9.0 KiB
TypeScript
253 lines
9.0 KiB
TypeScript
"use client";
|
||
|
||
import type { ReactNode } from "react";
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { ChevronDown, Settings } from "lucide-react";
|
||
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuLabel,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { GlobalSearch } from "@/components/global-search";
|
||
import { headerSubtitle } from "@/components/header-context";
|
||
|
||
/**
|
||
* 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
|
||
*
|
||
* Editorial/judicial aesthetic preserved:
|
||
* - Navy background with a gold hairline rule (border-b-3)
|
||
* - Parchment text, gold accents on hover/active
|
||
* - Hebrew RTL throughout (set on <html> in layout.tsx)
|
||
* - Active item gets `aria-current="page"` and a gold underline anchored
|
||
* to the bottom border, so screen readers announce the section and
|
||
* sighted users see where they are.
|
||
*/
|
||
|
||
type NavItem = { href: string; label: string };
|
||
type NavGroup = { id: string; items: NavItem[] };
|
||
|
||
const NAV_GROUPS: NavGroup[] = [
|
||
{
|
||
id: "work",
|
||
items: [
|
||
{ href: "/", label: "בית" },
|
||
{ href: "/archive", label: "ארכיון" },
|
||
],
|
||
},
|
||
{
|
||
id: "knowledge",
|
||
items: [
|
||
{ href: "/precedents", label: "ספריית פסיקה" },
|
||
{ href: "/training", label: "אימון סגנון" },
|
||
{ href: "/methodology", label: "מתודולוגיה" },
|
||
],
|
||
},
|
||
];
|
||
|
||
const ADMIN_ITEMS: NavItem[] = [
|
||
{ href: "/skills", label: "מיומנויות" },
|
||
{ href: "/diagnostics", label: "אבחון" },
|
||
{ href: "/settings", label: "הגדרות" },
|
||
];
|
||
|
||
type AgentBoard = { prefix: string; label: string; hint: string };
|
||
|
||
const AGENT_BOARDS: AgentBoard[] = [
|
||
{ prefix: "CMP", label: "רישוי ובניה", hint: "תיקי 1xxx" },
|
||
{ prefix: "CMPA", label: "היטלי השבחה", hint: "תיקי 8xxx / 9xxx" },
|
||
];
|
||
|
||
const PAPERCLIP_BASE = "https://pc.nautilus.marcusgroup.org";
|
||
|
||
function isActive(pathname: string, href: string): boolean {
|
||
if (href === "/") return pathname === "/";
|
||
return pathname === href || pathname.startsWith(`${href}/`);
|
||
}
|
||
|
||
export function AppShell({ children }: { children: ReactNode }) {
|
||
const pathname = usePathname();
|
||
const subtitle = headerSubtitle(pathname);
|
||
const adminActive = ADMIN_ITEMS.some((i) => isActive(pathname, i.href));
|
||
|
||
return (
|
||
<>
|
||
<header
|
||
className="
|
||
relative z-10 flex flex-col
|
||
bg-navy text-parchment
|
||
border-b-[3px] border-gold
|
||
shadow-md
|
||
"
|
||
>
|
||
{/* ─── Row 1 — brand bar (3-column grid) ─── */}
|
||
{/* Side columns flex 1fr each so the search column stays centered on
|
||
the viewport regardless of how wide the brand or agent labels grow. */}
|
||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(280px,460px)_minmax(0,1fr)] items-center gap-4 px-10 pt-[14px] pb-2">
|
||
<Link
|
||
href="/"
|
||
className="flex items-baseline gap-3 hover:text-parchment min-w-0 justify-self-start"
|
||
>
|
||
<span className="font-display text-[1.45rem] font-bold tracking-[0.02em] text-parchment whitespace-nowrap">
|
||
עוזר משפטי
|
||
</span>
|
||
<span
|
||
className="text-gold-soft text-sm font-medium truncate"
|
||
aria-live="polite"
|
||
>
|
||
{subtitle}
|
||
</span>
|
||
</Link>
|
||
|
||
<div className="w-full justify-self-center">
|
||
<GlobalSearch />
|
||
</div>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
className="
|
||
justify-self-end flex items-baseline gap-2 px-3 py-1.5 rounded
|
||
transition-colors outline-none
|
||
text-parchment/80 hover:text-parchment hover:bg-navy-soft/60
|
||
focus-visible:ring-2 focus-visible:ring-gold/60
|
||
data-[state=open]:bg-navy-soft/80 data-[state=open]:text-parchment
|
||
"
|
||
aria-label="ניהול סוכנים — בחר ועדה"
|
||
>
|
||
<span className="font-display text-[1.45rem] font-bold tracking-[0.02em] text-parchment whitespace-nowrap">
|
||
ניהול סוכנים
|
||
</span>
|
||
<ChevronDown className="size-4 self-center text-gold-soft" aria-hidden="true" />
|
||
</DropdownMenuTrigger>
|
||
|
||
<DropdownMenuContent align="end" sideOffset={10} className="min-w-[240px]">
|
||
<DropdownMenuLabel className="text-xs text-muted-foreground text-center">
|
||
Paperclip פתח דאשבורד
|
||
</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
{AGENT_BOARDS.map((board) => (
|
||
<DropdownMenuItem key={board.prefix} asChild>
|
||
<a
|
||
href={`${PAPERCLIP_BASE}/${board.prefix}/dashboard`}
|
||
target="_blank"
|
||
rel="noreferrer noopener"
|
||
className="flex flex-col gap-0.5 cursor-pointer py-1.5"
|
||
>
|
||
<span className="font-medium whitespace-nowrap">{board.label}</span>
|
||
<span className="text-xs text-muted-foreground tracking-wide whitespace-nowrap">
|
||
<span className="font-mono">{board.prefix}</span> · {board.hint}
|
||
</span>
|
||
</a>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
|
||
{/* ─── 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>
|
||
))}
|
||
</nav>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
className={`
|
||
relative ms-auto shrink-0 flex items-center gap-1.5
|
||
px-3 py-1.5 rounded text-sm transition-colors outline-none
|
||
focus-visible:ring-2 focus-visible:ring-gold/60
|
||
${adminActive
|
||
? "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="הגדרות מערכת"
|
||
>
|
||
<Settings className="size-4" aria-hidden="true" />
|
||
<ChevronDown className="size-3" aria-hidden="true" />
|
||
{adminActive && (
|
||
<span
|
||
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
|
||
aria-hidden="true"
|
||
/>
|
||
)}
|
||
</DropdownMenuTrigger>
|
||
|
||
<DropdownMenuContent align="end" sideOffset={10} className="min-w-[180px]">
|
||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||
מערכת
|
||
</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
{ADMIN_ITEMS.map((item) => {
|
||
const active = isActive(pathname, item.href);
|
||
return (
|
||
<DropdownMenuItem key={item.href} asChild>
|
||
<Link
|
||
href={item.href}
|
||
aria-current={active ? "page" : undefined}
|
||
className={`cursor-pointer ${active ? "font-semibold" : ""}`}
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
</DropdownMenuItem>
|
||
);
|
||
})}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
</header>
|
||
|
||
<main
|
||
id="main"
|
||
className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10"
|
||
>
|
||
{children}
|
||
</main>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function NavLink({ item, active }: { item: NavItem; active: boolean }) {
|
||
return (
|
||
<Link
|
||
href={item.href}
|
||
aria-current={active ? "page" : undefined}
|
||
className={`
|
||
relative px-3 py-1.5 rounded text-sm transition-colors
|
||
${active
|
||
? "text-parchment font-semibold bg-navy-soft/80"
|
||
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"}
|
||
`}
|
||
>
|
||
{item.label}
|
||
{active && (
|
||
<span
|
||
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
|
||
aria-hidden="true"
|
||
/>
|
||
)}
|
||
</Link>
|
||
);
|
||
}
|