feat(timeline): add pulse animation for in-flight drag mutations

Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 13:28:46 +02:00
parent 7a5e98e2e9
commit 1df208dbcc
386 changed files with 657 additions and 81650 deletions
@@ -1,12 +1,13 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
import { useState } from "react";
import { OrderType, AllocationType, ProjectStatus } from "@capakraken/shared";
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
import type { Project } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { DateInput } from "~/components/ui/DateInput.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { toDateInputValue } from "~/lib/format.js";
const ORDER_TYPE_OPTIONS = [
{ value: "BD", label: "BD" },
@@ -28,14 +29,6 @@ const STATUS_OPTIONS = [
{ value: "CANCELLED", label: "Cancelled" },
] as const;
function formatDateForInput(date: Date | string): string {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
interface FormState {
shortCode: string;
name: string;
@@ -54,7 +47,7 @@ interface FormState {
}
function getDefaultForm(): FormState {
const today = formatDateForInput(new Date());
const today = toDateInputValue(new Date());
return {
shortCode: "",
name: "",
@@ -81,8 +74,8 @@ function projectToForm(project: Project): FormState {
allocationType: project.allocationType,
winProbability: String(project.winProbability),
budgetEur: String(Math.round(project.budgetCents) / 100),
startDate: formatDateForInput(project.startDate),
endDate: formatDateForInput(project.endDate),
startDate: toDateInputValue(project.startDate),
endDate: toDateInputValue(project.endDate),
status: project.status,
responsiblePerson: project.responsiblePerson ?? "",
color: (project as unknown as { color?: string | null }).color ?? "",
@@ -108,9 +101,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});
const [serverError, setServerError] = useState<string | null>(null);
const panelRef = useRef<HTMLDivElement>(null);
useFocusTrap(panelRef, true);
const { data: utilizationCategories } = trpc.utilizationCategory.list.useQuery(undefined, { staleTime: 60_000 });
const { data: clientList } = trpc.clientEntity.list.useQuery(undefined, { staleTime: 60_000 });
@@ -139,15 +129,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
const isLoading = createMutation.isPending || updateMutation.isPending;
// Close on Escape key
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
function setField<K extends keyof FormState>(key: K, value: FormState[K]) {
setForm((prev) => ({ ...prev, [key]: value }));
setErrors((prev) => ({ ...prev, [key]: undefined }));
@@ -246,17 +227,8 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
const labelClass = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1";
return (
<div
className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center overflow-y-auto py-8"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
ref={panelRef}
className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-xl mx-4"
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<AnimatedModal open={true} onClose={onClose} maxWidth="max-w-xl" className="mx-4">
<div>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
@@ -609,6 +581,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
</div>
</form>
</div>
</div>
</AnimatedModal>
);
}