feat: timeline multi-select, demand popover, resource hover card, merged tooltips, dark mode fixes
Major timeline enhancements: - Right-click drag multi-selection with floating action bar (batch delete/assign) - DemandPopover for demand strip details (replaces broken "Loading" modal) - ResourceHoverCard on name hover showing skills, rates, role, chapter - Merged heatmap+vacation tooltips into unified TimelineTooltip component - Fixed overbooking blink animation (date normalization, z-index ordering) - Fixed dark mode sticky column bleed-through in project view - System roles admin page, notification task management, performance review docs Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -6,7 +6,7 @@ import Link from "next/link";
|
||||
import type { Route } from "next";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { clsx } from "clsx";
|
||||
import { Suspense, useState } from "react";
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
import { PreferencesModal } from "./PreferencesModal.js";
|
||||
import { ThemeProvider } from "./ThemeProvider.js";
|
||||
import { NotificationBell } from "../notifications/NotificationBell.js";
|
||||
@@ -139,6 +139,7 @@ const adminNavEntries: AdminEntry[] = [
|
||||
},
|
||||
{ href: "/admin/calculation-rules", label: "Calc. Rules", icon: <AdminIcon /> },
|
||||
{ href: "/admin/users", label: "Users", icon: <AdminIcon /> },
|
||||
{ href: "/admin/system-roles", label: "System Roles", icon: <AdminIcon /> },
|
||||
{ href: "/admin/settings", label: "Settings", icon: <AdminIcon /> },
|
||||
{ href: "/admin/skill-import", label: "Skill Import", icon: <AdminIcon /> },
|
||||
{ href: "/admin/notifications", label: "Broadcasts", icon: <BroadcastIcon /> },
|
||||
@@ -180,6 +181,15 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
const pathname = usePathname();
|
||||
const [prefsOpen, setPrefsOpen] = useState(false);
|
||||
|
||||
// Memoize active href set — avoids O(n²) on every render
|
||||
const activeHrefSet = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
for (const href of ALL_NAV_HREFS) {
|
||||
if (isNavItemActive(pathname, href)) set.add(href);
|
||||
}
|
||||
return set;
|
||||
}, [pathname]);
|
||||
|
||||
const visibleSections = navSections
|
||||
.map((section) => ({
|
||||
...section,
|
||||
@@ -194,13 +204,13 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
const initial: Record<string, boolean> = {};
|
||||
for (const section of visibleSections) {
|
||||
if (section.collapsed) {
|
||||
const hasActiveRoute = section.items.some((item) => isNavItemActive(pathname, item.href));
|
||||
const hasActiveRoute = section.items.some((item) => activeHrefSet.has(item.href));
|
||||
initial[section.label] = !hasActiveRoute;
|
||||
}
|
||||
}
|
||||
for (const entry of adminNavEntries) {
|
||||
if (isSubGroup(entry) && entry.collapsed) {
|
||||
const hasActiveRoute = entry.items.some((item) => isNavItemActive(pathname, item.href));
|
||||
const hasActiveRoute = entry.items.some((item) => activeHrefSet.has(item.href));
|
||||
initial[entry.label] = !hasActiveRoute;
|
||||
}
|
||||
}
|
||||
@@ -270,7 +280,7 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
href={item.href as Route}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all",
|
||||
isNavItemActive(pathname, item.href)
|
||||
activeHrefSet.has(item.href)
|
||||
? "bg-gradient-to-r from-brand-50 to-brand-100/70 text-brand-800 shadow-sm ring-1 ring-brand-200/70 dark:from-brand-900/30 dark:to-brand-800/20 dark:text-brand-200 dark:ring-brand-900/40"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
@@ -325,7 +335,7 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
href={item.href as Route}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-2xl px-3 py-1.5 text-sm font-medium transition-all",
|
||||
isNavItemActive(pathname, item.href)
|
||||
activeHrefSet.has(item.href)
|
||||
? "bg-gradient-to-r from-brand-50 to-brand-100/70 text-brand-800 shadow-sm ring-1 ring-brand-200/70 dark:from-brand-900/30 dark:to-brand-800/20 dark:text-brand-200 dark:ring-brand-900/40"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
@@ -344,7 +354,7 @@ function Sidebar({ userRole, onChatOpen }: { userRole: string; onChatOpen: () =>
|
||||
href={entry.href as Route}
|
||||
className={clsx(
|
||||
"group flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all",
|
||||
isNavItemActive(pathname, entry.href)
|
||||
activeHrefSet.has(entry.href)
|
||||
? "bg-gradient-to-r from-brand-50 to-brand-100/70 text-brand-800 shadow-sm ring-1 ring-brand-200/70 dark:from-brand-900/30 dark:to-brand-800/20 dark:text-brand-200 dark:ring-brand-900/40"
|
||||
: "text-gray-700 hover:bg-gray-100/90 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user