diff --git a/package.json b/package.json index 57059296..91ebacc8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@formkit/auto-animate": "^0.9.0", "@hookform/resolvers": "^5.2.2", "@pdfme/common": "^6.1.2", "@pdfme/generator": "^6.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b97620d7..a5806093 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.2.6) + '@formkit/auto-animate': + specifier: ^0.9.0 + version: 0.9.0 '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.75.0(react@19.2.6)) @@ -786,6 +789,9 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@formkit/auto-animate@0.9.0': + resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==} + '@hookform/devtools@4.4.0': resolution: {integrity: sha512-Mtlic+uigoYBPXlfvPBfiYYUZuyMrD3pTjDpVIhL6eCZTvQkHsKBSKeZCvXWUZr8fqrkzDg27N+ZuazLKq6Vmg==} peerDependencies: @@ -5890,6 +5896,8 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@formkit/auto-animate@0.9.0': {} + '@hookform/devtools@4.4.0(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.6) diff --git a/src/components/alerts/alert-rail.tsx b/src/components/alerts/alert-rail.tsx index 3286d541..8f2f3963 100644 --- a/src/components/alerts/alert-rail.tsx +++ b/src/components/alerts/alert-rail.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import { ArrowRight } from 'lucide-react'; +import { useAutoAnimate } from '@formkit/auto-animate/react'; import { useUIStore } from '@/stores/ui-store'; import { Skeleton } from '@/components/ui/skeleton'; @@ -18,6 +19,11 @@ export function AlertRail() { const visible = alerts.slice(0, 5); const overflow = Math.max(alerts.length - visible.length, 0); + // Smooth enter/leave for alerts as new ones arrive via socket realtime + // and stale ones get dismissed — replaces the jarring "card just + // appears/disappears" with a subtle fade+slide. + const [animateRef] = useAutoAnimate(); + return (
) : ( -
+
{visible.map((a) => ( ))} diff --git a/src/components/dashboard/my-reminders-rail.tsx b/src/components/dashboard/my-reminders-rail.tsx index 2ded8a59..97ebf544 100644 --- a/src/components/dashboard/my-reminders-rail.tsx +++ b/src/components/dashboard/my-reminders-rail.tsx @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation'; import { useQuery } from '@tanstack/react-query'; import { formatDistanceToNowStrict, isAfter, isBefore } from 'date-fns'; import { AlarmClock, ChevronRight } from 'lucide-react'; +import { useAutoAnimate } from '@formkit/auto-animate/react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -58,6 +59,10 @@ export function MyRemindersRail() { .slice(0, 6); const overdueCount = items.filter((r) => isBefore(new Date(r.dueAt), now)).length; + // Smooth animation when reminders complete / get dismissed / arrive + // via socket realtime. + const [animateRef] = useAutoAnimate(); + function hrefFor(r: ReminderRow): string { if (r.interestId) return `/${portSlug}/interests/${r.interestId}`; if (r.clientId) return `/${portSlug}/clients/${r.clientId}`; @@ -105,7 +110,7 @@ export function MyRemindersRail() { All caught up - no reminders.

) : ( -
    +
      {sorted.map((r) => { const due = new Date(r.dueAt); const isOverdue = isBefore(due, now); diff --git a/src/components/shared/notes-list.tsx b/src/components/shared/notes-list.tsx index 6ba5eb9c..ecab520c 100644 --- a/src/components/shared/notes-list.tsx +++ b/src/components/shared/notes-list.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { formatDistanceToNow } from 'date-fns'; import { Lock, Pencil, Trash2, Send, Loader2 } from 'lucide-react'; +import { useAutoAnimate } from '@formkit/auto-animate/react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; @@ -133,6 +134,10 @@ export function NotesList({ entityType, entityId, currentUserId, aggregate }: No const listEndpoint = aggregateOn ? `${baseEndpoint}?aggregate=true` : baseEndpoint; const queryKey = [entityType, entityId, 'notes', aggregateOn ? 'aggregated' : 'own']; + // Smooth animation when notes are added / edited / deleted — replaces + // the abrupt re-render with a per-row fade/slide. + const [animateRef] = useAutoAnimate(); + const { data: notes = [], isLoading } = useQuery({ queryKey, queryFn: () => apiFetch<{ data: Note[] }>(listEndpoint).then((r) => r.data), @@ -241,7 +246,7 @@ export function NotesList({ entityType, entityId, currentUserId, aggregate }: No ) : notes.length === 0 ? (
      No notes yet
      ) : ( -
      +
      {(groupBySource ? sortByGroup(notes) : notes).map((note) => (