# 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 |