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:
2026-03-18 23:43:51 +01:00
parent d0f04f13f8
commit ddec3a927a
67 changed files with 4930 additions and 1166 deletions
@@ -35,7 +35,7 @@ const TABS: Array<{ id: WorkspaceTab; label: string }> = [
function EmptyState({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-3xl border border-dashed border-gray-200 bg-white px-5 py-10 text-center text-sm text-gray-400">
<div className="rounded-3xl border border-dashed border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-5 py-10 text-center text-sm text-gray-400">
{children}
</div>
);
@@ -53,8 +53,8 @@ function ActionNotice({
className={clsx(
"rounded-2xl border px-4 py-3 text-sm",
tone === "success"
? "border-emerald-200 bg-emerald-50 text-emerald-800"
: "border-rose-200 bg-rose-50 text-rose-800",
? "border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300"
: "border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-800 dark:bg-rose-950/50 dark:text-rose-300",
)}
>
{children}
@@ -182,7 +182,7 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
<div className="mx-auto max-w-7xl space-y-6 p-6">
<Link
href="/estimates"
className="inline-flex items-center gap-1 text-sm text-gray-500 transition-colors hover:text-gray-800"
className="inline-flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400 transition-colors hover:text-gray-800 dark:hover:text-gray-200"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
@@ -190,21 +190,21 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
Back to Estimates
</Link>
<div className="rounded-[28px] border border-gray-200 bg-gradient-to-br from-white via-white to-brand-50 p-6 shadow-sm">
<div className="rounded-[28px] border border-gray-200 dark:border-gray-700 bg-gradient-to-br from-white via-white to-brand-50 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900 p-6 shadow-sm dark:shadow-black/20">
<div className="flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600">Estimate Workspace <InfoTooltip content="Central workspace for inspecting and editing an estimate's scope, staffing, financials, and version history." /></p>
<h1 className="mt-2 text-3xl font-semibold text-gray-900">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600 dark:text-sky-400">Estimate Workspace <InfoTooltip content="Central workspace for inspecting and editing an estimate's scope, staffing, financials, and version history." /></p>
<h1 className="mt-2 text-3xl font-semibold text-gray-900 dark:text-gray-50">
{estimate?.name ?? "Loading estimate"}
</h1>
<p className="mt-2 max-w-3xl text-sm text-gray-600">
<p className="mt-2 max-w-3xl text-sm text-gray-600 dark:text-gray-300">
Use the tabs below to inspect the connected estimate structure, version context, and staffing breakdown without relying on spreadsheet tabs.
</p>
</div>
{estimate && (
<div className="flex flex-col gap-3 lg:items-end">
<div className="grid gap-2 text-sm text-gray-500 lg:text-right">
<div className="grid gap-2 text-sm text-gray-500 dark:text-gray-400 lg:text-right">
<span>{estimate.project ? `${estimate.project.shortCode} - ${estimate.project.name}` : "Standalone estimate"}</span>
<span>Updated {formatDateLong(estimate.updatedAt)}</span>
</div>
@@ -215,7 +215,7 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
if (!editableTab && !isEditing) return;
setIsEditing((current) => !current);
}}
className="rounded-2xl border border-brand-200 bg-white px-4 py-2 text-sm font-semibold text-brand-700 transition hover:border-brand-300 hover:bg-brand-50"
className="rounded-2xl border border-brand-200 dark:border-sky-700 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-semibold text-brand-700 dark:text-sky-300 transition hover:border-brand-300 dark:hover:border-sky-600 hover:bg-brand-50 dark:hover:bg-gray-700"
>
{isEditing ? "Close editor" : editableTab ? "Edit working draft" : "Draft editor available in editable tabs"}
</button>
@@ -238,7 +238,7 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
{actionMessage && <ActionNotice tone="success">{actionMessage}</ActionNotice>}
{actionError && <ActionNotice tone="error">{actionError}</ActionNotice>}
<div className="flex flex-wrap gap-2 border-b border-gray-200">
<div className="flex flex-wrap gap-2 border-b border-gray-200 dark:border-gray-700">
{TABS.map((item) => (
<button
key={item.id}
@@ -247,8 +247,8 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
className={clsx(
"rounded-t-2xl border-b-2 px-4 py-3 text-sm font-medium transition-colors",
tab === item.id
? "border-brand-600 text-brand-700"
: "border-transparent text-gray-500 hover:text-gray-800",
? "border-brand-600 text-brand-700 dark:border-sky-400 dark:text-sky-300"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200",
)}
>
{item.label}