diff --git a/openframe-frontend-core/src/components/features/board/index.ts b/openframe-frontend-core/src/components/features/board/index.ts index c3be134a7..6a85380a6 100644 --- a/openframe-frontend-core/src/components/features/board/index.ts +++ b/openframe-frontend-core/src/components/features/board/index.ts @@ -21,6 +21,8 @@ export type { BoardColumnDef, BoardPriority, BoardTicket, + BoardTicketActivity, + BoardTicketActivityKind, BoardTicketAssignee, BoardTicketPendingApproval, } from './types'; diff --git a/openframe-frontend-core/src/components/features/board/ticket-card.tsx b/openframe-frontend-core/src/components/features/board/ticket-card.tsx index 3960e37fe..d0cf19fbc 100644 --- a/openframe-frontend-core/src/components/features/board/ticket-card.tsx +++ b/openframe-frontend-core/src/components/features/board/ticket-card.tsx @@ -21,7 +21,14 @@ import { cn } from '../../../utils/cn'; import { formatTicketRelativeTime, formatTicketFullTimestamp } from '../../../utils/date-utils'; import { holdMoveDragEffect } from '../../../utils/drag-effect'; import { getReadableTextColor } from '../../../utils/ods-color-utils'; -import { LaptopIcon, Flag02Icon, MessagesIcon, UserCheckIcon } from '../../icons-v2-generated'; +import { + ClockIcon, + DotsLoaderIcon, + LaptopIcon, + Flag02Icon, + MessagesIcon, + UserCheckIcon, +} from '../../icons-v2-generated'; import { DeletedUserAvatar } from '../../ui/deleted-user-avatar'; import { SquareAvatar } from '../../ui/square-avatar'; import { Tag } from '../../ui/tag'; @@ -30,7 +37,7 @@ import { BoardTicketApproval } from './board-ticket-approval'; import { useBoardLift, useDropAim } from './drop-aim'; import { DROP_LINE_ATTRIBUTE } from './lane-geometry'; import { useIsLanding } from './pending-move'; -import type { BoardPriority, BoardTicket } from './types'; +import type { BoardPriority, BoardTicket, BoardTicketActivityKind } from './types'; import { TICKET_ID_ATTRIBUTE } from './use-lane-scroll-anchor'; const PRIORITY_COLOR_CLASS: Record = { @@ -50,6 +57,13 @@ export const DRAG_PREVIEW_OPACITY = 0.9; const MAX_VISIBLE_TAGS = 2; const MAX_VISIBLE_ASSIGNEES = 3; +const ACTIVITY_DEFAULT_LABEL: Record = { + 'ai-working': 'AI assistant is working', + 'user-typing': 'User typing', + 'waiting-external': 'Waiting for client response', + stale: 'No activity', +}; + /** Shared card shell (border / padding / bg). Same footprint for the draggable * board card and the static {@link TicketCardView}. */ const TICKET_CARD_SHELL = @@ -158,6 +172,21 @@ export function TicketCardBody({ ticket, columnColor, renderAssignSlot, onApprov Escalated by User )} + {ticket.activity && ( +
+ {ticket.activity.kind === 'stale' ? ( + + ) : ( + + )} + {ticket.activity.label ?? ACTIVITY_DEFAULT_LABEL[ticket.activity.kind]} +
+ )} {showNewMessage && ( { + const activities: { kind: BoardTicketActivityKind; label?: string }[] = [ + { kind: 'ai-working' }, + { kind: 'user-typing' }, + { kind: 'waiting-external' }, + { kind: 'stale', label: 'No activity for 2 hours' }, + ]; + return ( +
+ {activities.map(activity => ( + + ))} +
+ ); + }, +}; + /** * Pending CLIENT approval — collapsed grey "Pending client approval" row with a * loader glyph. Expand it to reach the shared approve/reject affordance.