# Contact Activity Panel URL: /docs/web/components/contact-activity-panel A presentational record-activity timeline. Pair it with useDocyrusContactActivity to wire it to the Docyrus backend. **Demo:** ```tsx 'use client'; // @custom-demo import { useState } from 'react'; import { ContactActivityPanel } from '@docyrus/ui/components/contact-activity-panel'; import { Button } from '@docyrus/ui/primitives/ui/button'; import { cn } from '@docyrus/ui/primitives/lib/utils'; import { PropControls } from '@/components/prop-controls'; import { useDemoData } from '@/data/contact-activity-panel-data'; export function ContactActivityPanelDemo() { const { props, controls, actions } = useDemoData(); const [open, setOpen] = useState(false); return (
); } ``` ## 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`](/docs/web/hooks/use-docyrus-contact-activity), 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: ```tsx const activity = useDocyrusContactActivity({ client, appSlug, dataSourceSlug, recordId, relationSlug: 'contact' }); ``` 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`](/docs/web/components/log-activity-form) (wired via `useDocyrusLogActivity`). ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-contact-activity-panel ``` **Dependencies:** - [lucide-react](https://www.npmjs.com/package/lucide-react) ## Usage ```tsx 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"; openComposer(type)} renderAiAssistant={({ open, 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`. View controls (`width`, `iconStyle`, `showHeader`) are props; dark mode comes from the global `DocyThemeProvider`. ## 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` | ## 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 | | `ChatUser` | — | | `ChatPost` | — | | `PostAttachment` | — | | `PostReaction` | — | | `CreatePostPayload` | — | | `ActivityTypeConfig` | — | ## Type Reference ### ContactActivity | Field | Type | Description | |-------|------|-------------| | `id` | `string` | — | | `type` | `ActivityType` | — | | `subject` | `string` | — | | `description` | `string` | — | | `metadata` | `Record` | — | | `attachments` | `Array \| null` | — | | `reactions` | `Array` | — | | `title` | `string` | Display title; falls back to `subject` | | `preview` | `string` | One-line body preview; falls back to first line of `description` | | `participants` | `Array` | 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` | Detail-modal key points (consumer-supplied) | | `followUps` | `Array<{ id: string; text: string; done?: boolean }>` | Detail-modal follow-ups (consumer-supplied) | | `related` | `Array<{ id?: string; label: string }>` | Related-record chips (consumer-supplied) | | `primaryAction` | `{ label: string; onClick?: () => void }` | Detail-modal primary action | ### ContactActivityPanelProps | Field | Type | Description | |-------|------|-------------| | `activities` | `Array` | Timeline entries | | `currentUser` | `ChatUser` | — | | `users` | `Array` | 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` | Upcoming section (region 4) | | `onToggleUpcomingTask` | `(id: string) => void \| Promise` | Upcoming task check toggle | | `addActions` | `Array` | Add-menu entries (region 3) | | `onAddActivity` | `(type: string) => void` | Emitted when an Add entry is picked | | `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` | 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` | — | | `onCreateComment` | `(payload: CreatePostPayload) => void \| Promise` | Comment reply create (detail modal) | | `onDeleteComment` | `(commentId: string) => void \| Promise` | Comment delete | | `onLoadReplies` | `(activityId: string) => Promise>` | Loads a comment's replies (detail modal) | | `dataSources` | `Array` | — | | `onSearchEntity` | `(dataSourceId: string, query: string) => Promise>` | — | | `activityTypes` | `Array` | Restrict the filterable types | | `maxHeight` | `number \| string` | — | | `className` | `string` | — | ### ActivityType `"call" \| "meeting" \| "email" \| "chat" \| "sms" \| "whatsapp" \| "linkedin" \| "note" \| "comment" \| "task" \| "status_update" \| "status" \| "file" \| "form" \| "record" \| "trace" \| "record_create" \| "record_update"` ### 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 |