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,7 +1,7 @@
"use client";
import { useRef, useState } from "react";
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
import { useState } from "react";
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
import { VacationType } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { DateInput } from "~/components/ui/DateInput.js";
@@ -85,9 +85,6 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
const [halfDayPart, setHalfDayPart] = useState<"MORNING" | "AFTERNOON">("MORNING");
const [serverError, setServerError] = useState<string | null>(null);
const panelRef = useRef<HTMLDivElement>(null);
useFocusTrap(panelRef, true);
const debouncedStart = useDebounce(startDate, 400);
const debouncedEnd = useDebounce(endDate, 400);
@@ -139,6 +136,7 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
const utils = trpc.useUtils();
// @ts-ignore TS2589: tRPC infers union type too deeply for CreateVacationRequestSchema with .superRefine()
const createMutation = trpc.vacation.create.useMutation({
onSuccess: async () => {
await utils.vacation.list.invalidate();
@@ -182,17 +180,8 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
const resourceList: { id: string; displayName: string; eid: string }[] = resources?.resources ?? [];
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-lg mx-4"
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<AnimatedModal open={true} onClose={onClose} maxWidth="max-w-lg" 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">Request Vacation</h2>
@@ -467,6 +456,6 @@ export function VacationModal({ resourceId: initialResourceId, onClose, onSucces
</div>
</form>
</div>
</div>
</AnimatedModal>
);
}
@@ -1,5 +1,7 @@
"use client";
import { MILLISECONDS_PER_DAY } from "@capakraken/shared";
export const HOLIDAY_SOURCE_LABELS = {
CALENDAR: "Holiday Calendar",
LEGACY_PUBLIC_HOLIDAY: "Legacy import",
@@ -32,7 +34,7 @@ export function getRequestedDays(vacation: Pick<VacationExplainabilityEntry, "st
const start = new Date(vacation.startDate);
const end = new Date(vacation.endDate);
return Math.round((end.getTime() - start.getTime()) / 86_400_000) + 1;
return Math.round((end.getTime() - start.getTime()) / MILLISECONDS_PER_DAY) + 1;
}
export function getHolidayBasis(vacation: VacationExplainabilityEntry): string[] {