From 554b95779f33531d642c248dfb822c1253799ce2 Mon Sep 17 00:00:00 2001 From: ephraimduncan Date: Tue, 7 Jul 2026 08:42:05 +0000 Subject: [PATCH] feat(ui): redesign recipient avatar stack hover popover Match the recipient field hover card style: status dot + colored label with count per section, compact 32px avatar rows with medium primary text, subtle section dividers, and a hover pill with copy icon on interactive rows. --- .../general/avatar-with-recipient.tsx | 21 ++- .../app/components/general/stack-avatar.tsx | 13 +- .../general/stack-avatars-with-tooltip.tsx | 153 +++++++++--------- 3 files changed, 99 insertions(+), 88 deletions(-) diff --git a/apps/remix/app/components/general/avatar-with-recipient.tsx b/apps/remix/app/components/general/avatar-with-recipient.tsx index 53f5eae6e..96ce911e8 100644 --- a/apps/remix/app/components/general/avatar-with-recipient.tsx +++ b/apps/remix/app/components/general/avatar-with-recipient.tsx @@ -9,6 +9,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { DocumentStatus } from '@prisma/client'; +import { CopyIcon } from 'lucide-react'; import { StackAvatar } from './stack-avatar'; @@ -40,8 +41,8 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec return (
-
-

{recipient.email || recipient.name}

-

{_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}

+
+

{recipient.email || recipient.name}

+

+ {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)} +

+ + {signingToken && ( + + )}
); } diff --git a/apps/remix/app/components/general/stack-avatar.tsx b/apps/remix/app/components/general/stack-avatar.tsx index 48caed2ee..22eb096bc 100644 --- a/apps/remix/app/components/general/stack-avatar.tsx +++ b/apps/remix/app/components/general/stack-avatar.tsx @@ -1,4 +1,5 @@ import { RecipientStatusType } from '@documenso/lib/client-only/recipient-type'; +import { cn } from '@documenso/ui/lib/utils'; import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; const ZIndexes: { [key: string]: string } = { @@ -14,9 +15,10 @@ export type StackAvatarProps = { zIndex?: string; fallbackText?: string; type: RecipientStatusType; + className?: string; }; -export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAvatarProps) => { +export const StackAvatar = ({ first, zIndex, fallbackText = '', type, className }: StackAvatarProps) => { let classes = ''; let zIndexClass = ''; const firstClass = first ? '' : '-ml-3'; @@ -46,7 +48,14 @@ export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAva } return ( - + {fallbackText} ); diff --git a/apps/remix/app/components/general/stack-avatars-with-tooltip.tsx b/apps/remix/app/components/general/stack-avatars-with-tooltip.tsx index ef096afe6..aeea8670b 100644 --- a/apps/remix/app/components/general/stack-avatars-with-tooltip.tsx +++ b/apps/remix/app/components/general/stack-avatars-with-tooltip.tsx @@ -2,6 +2,7 @@ import { getRecipientType, RecipientStatusType } from '@documenso/lib/client-onl import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import type { TRecipientLite } from '@documenso/lib/types/recipient'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; +import { cn } from '@documenso/ui/lib/utils'; import { PopoverHover } from '@documenso/ui/primitives/popover'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; @@ -58,94 +59,90 @@ export const StackAvatarsWithTooltip = ({ ]; }, [recipients]); + const sections = [ + { + key: 'completed', + label: Completed, + dotClassName: 'bg-green-500', + labelClassName: 'text-green-600 dark:text-green-400', + recipients: completedRecipients, + isInteractive: false, + }, + { + key: 'rejected', + label: Rejected, + dotClassName: 'bg-red-500', + labelClassName: 'text-red-600 dark:text-red-400', + recipients: rejectedRecipients, + isInteractive: false, + }, + { + key: 'waiting', + label: Waiting, + dotClassName: 'bg-blue-500', + labelClassName: 'text-blue-600 dark:text-blue-400', + recipients: waitingRecipients, + isInteractive: true, + }, + { + key: 'opened', + label: Opened, + dotClassName: 'bg-amber-400', + labelClassName: 'text-amber-600 dark:text-amber-400', + recipients: openedRecipients, + isInteractive: true, + }, + { + key: 'uncompleted', + label: Uncompleted, + dotClassName: 'bg-gray-400', + labelClassName: 'text-muted-foreground', + recipients: uncompletedRecipients, + isInteractive: true, + }, + ].filter((section) => section.recipients.length > 0); + return ( } contentProps={{ - className: 'flex flex-col gap-y-5 py-2', + className: 'w-64 divide-y divide-border/50 overflow-hidden p-0 text-sm', side: position, }} > - {completedRecipients.length > 0 && ( -
-

- Completed -

- {completedRecipients.map((recipient) => ( -
- -
-

{recipient.email || recipient.name}

-

- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)} -

-
-
- ))} -
- )} + {sections.map((section) => ( +
+
+ + {section.label} + {section.recipients.length} +
- {rejectedRecipients.length > 0 && ( -
-

- Rejected -

- {rejectedRecipients.map((recipient) => ( -
- -
-

{recipient.email || recipient.name}

-

- {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)} -

-
-
- ))} -
- )} +
+ {section.isInteractive + ? section.recipients.map((recipient) => ( + + )) + : section.recipients.map((recipient) => ( +
+ - {waitingRecipients.length > 0 && ( -
-

- Waiting -

- {waitingRecipients.map((recipient) => ( - - ))} +
+

{recipient.email || recipient.name}

+

+ {_(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)} +

+
+
+ ))} +
- )} - - {openedRecipients.length > 0 && ( -
-

- Opened -

- {openedRecipients.map((recipient) => ( - - ))} -
- )} - - {uncompletedRecipients.length > 0 && ( -
-

- Uncompleted -

- {uncompletedRecipients.map((recipient) => ( - - ))} -
- )} + ))} ); };