Components

Contact Activity Panel

A presentational record-activity timeline. Pair it with useDocyrusContactActivity to wire it to the Docyrus backend.

Client Only

Backend wiring

ContactActivityPanel is presentational — it renders the timeline and emits callbacks, but owns no network I/O. For Docyrus-backed apps, drive it with useDocyrusContactActivity, which merges the record's audit log, threaded comments, related events, tasks, and status updates into one feed and returns a panelProps bag you spread straight onto the panel:

const activity = useDocyrusContactActivity({ client, appSlug, dataSourceSlug, recordId, relationSlug: 'contact' });

<ContactActivityPanel {...activity.panelProps} />

The barrel also re-exports the hook and its types from @docyrus/ui/components/contact-activity-panel. To compose a timeline and a "log activity" composer on the same record, pair it with LogActivityForm (wired via useDocyrusLogActivity).

Installation

pnpm dlx @docyrus/cli add @docyrus/ui-contact-activity-panel
UI Primitives(7 components)
npx shadcn@latest add avatar button dialog dropdown-menu popover scroller skeleton

Usage

import {
  ContactActivityPanel
} from "@docyrus/ui/components/contact-activity-panel";
import type {
  ContactActivity,
  ContactActivityPanelProps,
  ActivityType,
  ActivityCategory,
  UpcomingActivity,
  RelationshipSummary,
  ContactActivityContact,
  AddActivityAction,
  ChatUser,
  ChatPost,
  PostAttachment,
  PostReaction,
  CreatePostPayload,
  ActivityTypeConfig
} from "@docyrus/ui/components/contact-activity-panel";

<ContactActivityPanel
  contact={{ name: "John Smith", org: "Acme Corp" }}
  activities={activities}
  upcoming={upcoming}
  relationshipSummary={{ lastContactedAt, nextActivityAt, openTaskCount: 1 }}
  width="panel"
  iconStyle="color"
  addActions={addActions}
  onAddActivity={(type) => openComposer(type)}
  renderAddForm={({ type, close }) => <MyQuickAddForm type={type} onDone={close} />}
  renderAiAssistant={({ open, onOpenChange }) => <MyDocyDialog open={open} onOpenChange={onOpenChange} />}
/>

Redesign (v2)

The panel is a typed activity timeline: primary rows (calls, meetings, emails, chats, notes, comments) render as clickable cards that open a detail modal; secondary rows (status changes, files, form submissions, record updates, workflow traces) render as compact one-liners on a shared rail. A category tab bar (All / Communication / Tasks / Updates) plus a grouped type-filter popover scope the feed. Optional regions — a contact header, a relationship-summary + Add + Ask Docy row, and a collapsible Upcoming list — light up when their props are supplied. Ask Docy and the Add menu are slots/emit-only: the panel owns no AI wiring and never opens a composer itself — supply renderAiAssistant / onAddActivity, or hand the "+" a form with renderAddForm. View controls (width, iconStyle, showHeader) are props; dark mode comes from the global DocyThemeProvider.

Quick-add form

onAddActivity only announces the picked type. Pass renderAddForm as well and the panel opens your form in a popover under the "+" — the entry is created where the user clicked instead of in a separate region. The slot receives { type, close }; call close() after a successful submit. With exactly one addActions entry the trigger skips the menu and opens the form directly. useDocyrusLogActivity's logActivityProps drop straight into it (<LogActivityForm {...logActivityProps} activityTypes={[type]} activeType={type} />).

Timestamps

Timeline timestamps count up while the event is recent and become a date once it isn't: now → 5m → 3h → 2d → 3w → 21 Sep → 21 Sep 2025. The units are translation keys (ui.contactActivity.time.*), the absolute form uses the runtime locale, and the full date + time is always on the element's title.

Day groups

The timeline buckets entries by calendar boundaries, not rolling windows — Today, Yesterday, This week, Last week, This month, Last month, Earlier this year, Last year, Older. Empty groups never render. weekStartsOn (default 1, Monday) decides where a week begins.

Documents

An activity's attachments show as a paperclip marker on its row (with the size when there is exactly one) and as document cards in the detail modal — image thumbnails, a per-type icon and a download link for everything else. The Docyrus hook fills them from the record's files and from comment attachments.

Components

ComponentDescription
ContactActivityPanelPublic export
ContactActivityHeaderInternal — contact header (region 1)
ContactActivityToolbarInternal — category tabs + filter popover (region 2)
ContactActivitySummaryInternal — relationship summary + Add + Ask Docy (region 3)
ContactActivityUpcomingInternal — collapsible Upcoming list (region 4)
ActivityTimelineInternal — day-grouped timeline + rail
ActivityTimelineItemInternal — primary/secondary dispatch + rail chip
ActivityPrimaryCardInternal — clickable activity card
ActivitySecondaryRowInternal — compact one-liner
ActivityAvatarsInternal — participant avatar stack
ActivityAddMenuInternal — Add dropdown (emit-only)
ActivityDetailDialogInternal — activity detail modal (region 6)
ActivityEmptyStateInternal — activity-empty-state.tsx
ActivityLoadingSkeletonInternal — activity-skeleton.tsx
ContactActivityProviderInternal — contact-activity-context.tsx
ActivityAttachmentBadgeInternal — paperclip marker on a timeline row
ActivityAttachmentListInternal — document cards in the detail modal

Type Exports

TypeDescription
ContactActivityA single timeline entry (see fields below)
ContactActivityPanelPropsPanel props
ActivityTypeUnion of activity kinds
ActivityCategory"communication" | "tasks" | "updates"
UpcomingActivityA future task/meeting (Upcoming section)
RelationshipSummaryLast-contact / next-activity / open-task summary
ContactActivityContactHeader identity
AddActivityActionAn Add-menu entry
AddFormContext{ type, close } handed to renderAddForm
ActivityRelatedEntityA related record / person chip
ActivityDetailRowOne label/value line of the detail modal's Details block
DateGroupKeyTimeline day-group key
ChatUser—
ChatPost—
PostAttachment—
PostReaction—
CreatePostPayload—
ActivityTypeConfig—

Type Reference

ContactActivity

FieldTypeDescription
idstring—
typeActivityType—
subjectstring—
descriptionstring—
metadataRecord<string, unknown>—
attachmentsArray<PostAttachment> | null—
reactionsArray<PostReaction>—
titlestringDisplay title; falls back to subject
previewstringOne-line body preview; falls back to first line of description
participantsArray<string>User ids for the avatar stack (beyond created_by)
timeRange{ start: string; end?: string }Start/end for a "14:00–14:45" chip
dueDatestringTask due date
actorLabelstringActor label on secondary rows
summarystringDetail-modal summary (consumer-supplied)
keyPointsArray<string>Detail-modal key points (consumer-supplied)
followUpsArray<{ id: string; text: string; done?: boolean }>Detail-modal follow-ups (consumer-supplied)
relatedArray<ActivityRelatedEntity>Related-record chips (filled by the hook, or consumer-supplied)
primaryAction{ label: string; onClick?: () => void }Detail-modal primary action

ContactActivityPanelProps

FieldTypeDescription
activitiesArray<ContactActivity>Timeline entries
currentUserChatUser—
usersArray<ChatUser>Resolves participant avatars
contactContactActivityContactHeader identity (region 1)
showHeaderbooleanToggle the contact header (default true)
headerActionsReactNodeHeader ⋯ menu slot
width"panel" | "wide"Layout width (default "panel")
iconStyle"color" | "mono"Chip tone (default "color")
relationshipSummaryRelationshipSummarySummary line (region 3)
upcomingArray<UpcomingActivity>Upcoming section (region 4)
onToggleUpcomingTask(id: string) => void | Promise<void>Upcoming task check toggle
upcomingCollapsedCountnumberUpcoming entries shown before the "N more" control (default 4)
weekStartsOnnumberFirst day of the week for the day groups, 0 = Sunday (default 1)
addActionsArray<AddActivityAction>Add-menu entries (region 3)
onAddActivity(type: string) => voidEmitted when an Add entry is picked
renderAddForm(ctx: { type: string; close: () => void }) => ReactNodeQuick-add form opened in a popover under "+"
onActivityClick(activity: ContactActivity) => voidOverride the built-in detail modal
renderActivityDetail(activity: ContactActivity) => ReactNodeCustom detail-modal body
onToggleFollowUp(activityId: string, followUpId: string) => void | Promise<void>Follow-up check toggle
renderAiAssistant(ctx: { open: boolean; onOpenChange: (open: boolean) => void }) => ReactNodeAsk Docy slot (region 7)
aiAssistantOpenbooleanControlled Ask Docy open state
onAiAssistantOpenChange(open: boolean) => voidAsk Docy open-state change
isLoadingboolean—
isCreatePendingboolean—
isDeletePendingboolean—
onDeleteActivity(activityId: string) => void | Promise<void>—
onCreateComment(payload: CreatePostPayload) => void | Promise<void>Comment reply create (detail modal)
onDeleteComment(commentId: string) => void | Promise<void>Comment delete
onLoadReplies(activityId: string) => Promise<Array<ChatPost>>Loads a comment's replies (detail modal)
dataSourcesArray<DataSourceOption>—
onSearchEntity(dataSourceId: string, query: string) => Promise<Array<EntitySearchResult>>—
activityTypesArray<ActivityType>Restrict the filterable types
maxHeightnumber | string—
classNamestring—

ActivityRelatedEntity

FieldTypeDescription
idstringRelated record id
labelstringDisplay label
rolestringRelation the entity came from ("Assignee", "Project")
onClick() => voidOpens the record; the chip is inert without it

ActivityType

"call" \| "meeting" \| "email" \| "chat" \| "sms" \| "whatsapp" \| "linkedin" \| "note" \| "comment" \| "task" \| "status_update" \| "status" \| "file" \| "form" \| "record" \| "trace" \| "record_create" \| "record_update"

ActivityRelatedEntity

FieldTypeDescription
idstringRelated record id
labelstringDisplay label
rolestringRelation the entity came from ("Assignee", "Project")
onClick() => voidOpens the record; the chip is inert without it

ActivityTypeConfig

FieldTypeDescription
iconLucideIcon—
labelstring—
colorClassstring—
bgClassstring—
categoryActivityCategoryTab-bar grouping
colorHex{ light: string; dark: string }Colored-chip tint (absent → monochrome)
primarybooleantrue → card row; else secondary one-liner
compactbooleanLegacy record-row flag

On this page