Contact Activity Panel
A presentational record-activity timeline. Pair it with useDocyrusContactActivity to wire it to the Docyrus backend.
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-panelnpx shadcn@latest add avatar button dialog dropdown-menu popover scroller skeletonUsage
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
| Component | Description |
|---|---|
ContactActivityPanel | Public export |
ContactActivityHeader | Internal — contact header (region 1) |
ContactActivityToolbar | Internal — category tabs + filter popover (region 2) |
ContactActivitySummary | Internal — relationship summary + Add + Ask Docy (region 3) |
ContactActivityUpcoming | Internal — collapsible Upcoming list (region 4) |
ActivityTimeline | Internal — day-grouped timeline + rail |
ActivityTimelineItem | Internal — primary/secondary dispatch + rail chip |
ActivityPrimaryCard | Internal — clickable activity card |
ActivitySecondaryRow | Internal — compact one-liner |
ActivityAvatars | Internal — participant avatar stack |
ActivityAddMenu | Internal — Add dropdown (emit-only) |
ActivityDetailDialog | Internal — activity detail modal (region 6) |
ActivityEmptyState | Internal — activity-empty-state.tsx |
ActivityLoadingSkeleton | Internal — activity-skeleton.tsx |
ContactActivityProvider | Internal — contact-activity-context.tsx |
ActivityAttachmentBadge | Internal — paperclip marker on a timeline row |
ActivityAttachmentList | Internal — document cards in the detail modal |
Type Exports
| Type | Description |
|---|---|
ContactActivity | A single timeline entry (see fields below) |
ContactActivityPanelProps | Panel props |
ActivityType | Union of activity kinds |
ActivityCategory | "communication" | "tasks" | "updates" |
UpcomingActivity | A future task/meeting (Upcoming section) |
RelationshipSummary | Last-contact / next-activity / open-task summary |
ContactActivityContact | Header identity |
AddActivityAction | An Add-menu entry |
AddFormContext | { type, close } handed to renderAddForm |
ActivityRelatedEntity | A related record / person chip |
ActivityDetailRow | One label/value line of the detail modal's Details block |
DateGroupKey | Timeline day-group key |
ChatUser | — |
ChatPost | — |
PostAttachment | — |
PostReaction | — |
CreatePostPayload | — |
ActivityTypeConfig | — |
Type Reference
ContactActivity
| Field | Type | Description |
|---|---|---|
id | string | — |
type | ActivityType | — |
subject | string | — |
description | string | — |
metadata | Record<string, unknown> | — |
attachments | Array<PostAttachment> | null | — |
reactions | Array<PostReaction> | — |
title | string | Display title; falls back to subject |
preview | string | One-line body preview; falls back to first line of description |
participants | Array<string> | User ids for the avatar stack (beyond created_by) |
timeRange | { start: string; end?: string } | Start/end for a "14:00–14:45" chip |
dueDate | string | Task due date |
actorLabel | string | Actor label on secondary rows |
summary | string | Detail-modal summary (consumer-supplied) |
keyPoints | Array<string> | Detail-modal key points (consumer-supplied) |
followUps | Array<{ id: string; text: string; done?: boolean }> | Detail-modal follow-ups (consumer-supplied) |
related | Array<ActivityRelatedEntity> | Related-record chips (filled by the hook, or consumer-supplied) |
primaryAction | { label: string; onClick?: () => void } | Detail-modal primary action |
ContactActivityPanelProps
| Field | Type | Description |
|---|---|---|
activities | Array<ContactActivity> | Timeline entries |
currentUser | ChatUser | — |
users | Array<ChatUser> | Resolves participant avatars |
contact | ContactActivityContact | Header identity (region 1) |
showHeader | boolean | Toggle the contact header (default true) |
headerActions | ReactNode | Header ⋯ menu slot |
width | "panel" | "wide" | Layout width (default "panel") |
iconStyle | "color" | "mono" | Chip tone (default "color") |
relationshipSummary | RelationshipSummary | Summary line (region 3) |
upcoming | Array<UpcomingActivity> | Upcoming section (region 4) |
onToggleUpcomingTask | (id: string) => void | Promise<void> | Upcoming task check toggle |
upcomingCollapsedCount | number | Upcoming entries shown before the "N more" control (default 4) |
weekStartsOn | number | First day of the week for the day groups, 0 = Sunday (default 1) |
addActions | Array<AddActivityAction> | Add-menu entries (region 3) |
onAddActivity | (type: string) => void | Emitted when an Add entry is picked |
renderAddForm | (ctx: { type: string; close: () => void }) => ReactNode | Quick-add form opened in a popover under "+" |
onActivityClick | (activity: ContactActivity) => void | Override the built-in detail modal |
renderActivityDetail | (activity: ContactActivity) => ReactNode | Custom detail-modal body |
onToggleFollowUp | (activityId: string, followUpId: string) => void | Promise<void> | Follow-up check toggle |
renderAiAssistant | (ctx: { open: boolean; onOpenChange: (open: boolean) => void }) => ReactNode | Ask Docy slot (region 7) |
aiAssistantOpen | boolean | Controlled Ask Docy open state |
onAiAssistantOpenChange | (open: boolean) => void | Ask Docy open-state change |
isLoading | boolean | — |
isCreatePending | boolean | — |
isDeletePending | boolean | — |
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) |
dataSources | Array<DataSourceOption> | — |
onSearchEntity | (dataSourceId: string, query: string) => Promise<Array<EntitySearchResult>> | — |
activityTypes | Array<ActivityType> | Restrict the filterable types |
maxHeight | number | string | — |
className | string | — |
ActivityRelatedEntity
| Field | Type | Description |
|---|---|---|
id | string | Related record id |
label | string | Display label |
role | string | Relation the entity came from ("Assignee", "Project") |
onClick | () => void | Opens 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
| Field | Type | Description |
|---|---|---|
id | string | Related record id |
label | string | Display label |
role | string | Relation the entity came from ("Assignee", "Project") |
onClick | () => void | Opens the record; the chip is inert without it |
ActivityTypeConfig
| Field | Type | Description |
|---|---|---|
icon | LucideIcon | — |
label | string | — |
colorClass | string | — |
bgClass | string | — |
category | ActivityCategory | Tab-bar grouping |
colorHex | { light: string; dark: string } | Colored-chip tint (absent → monochrome) |
primary | boolean | true → card row; else secondary one-liner |
compact | boolean | Legacy record-row flag |