Initial commit
This commit is contained in:
100
src/components/ds/ActivityCard.jsx
Normal file
100
src/components/ds/ActivityCard.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Avatar } from '@/components/ds/Avatar';
|
||||
import { Surface } from '@/components/ds/Surface';
|
||||
import { StatusBadge } from '@/components/ds/StatusBadge';
|
||||
|
||||
/**
|
||||
* ActivityCard — one event in a feed: who did what, when.
|
||||
*
|
||||
* Renders as a row by default (feeds are dense) and as a bordered card when
|
||||
* `variant="card"`. Either way the anatomy is fixed — actor, sentence, meta,
|
||||
* trailing slot — so a feed built from mixed event types stays aligned.
|
||||
*/
|
||||
export function ActivityCard({
|
||||
/** Person or system that acted. */
|
||||
actor,
|
||||
actorSrc,
|
||||
/** Icon shown instead of an avatar, for system events. */
|
||||
icon: Icon,
|
||||
iconTone = 'soft',
|
||||
/** What happened — "Screened Sofia Mendez". */
|
||||
title,
|
||||
/** Supporting detail. */
|
||||
description,
|
||||
timestamp,
|
||||
status,
|
||||
/** Right-aligned slot — a score, a button. */
|
||||
trailing,
|
||||
variant = 'row',
|
||||
onClick,
|
||||
className,
|
||||
}) {
|
||||
const iconTones = {
|
||||
soft: 'bg-krow-blue-tint text-krow-blue',
|
||||
brand: 'bg-krow-blue text-white',
|
||||
success: 'bg-success-muted text-success',
|
||||
warning: 'bg-warning-muted text-warning',
|
||||
destructive: 'bg-destructive-muted text-destructive',
|
||||
neutral: 'bg-surface-sunken text-ink-3',
|
||||
};
|
||||
|
||||
const body = (
|
||||
<>
|
||||
{Icon ? (
|
||||
<span className={cn('grid place-items-center w-9 h-9 rounded-xl shrink-0', iconTones[iconTone])}>
|
||||
<Icon className="w-4 h-4" aria-hidden="true" />
|
||||
</span>
|
||||
) : (
|
||||
<Avatar name={actor} src={actorSrc} size="default" />
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-body-sm text-ink-1">
|
||||
{actor && <span className="font-semibold">{actor}</span>}
|
||||
{actor && title && ' '}
|
||||
{title && <span className={actor ? 'text-ink-2' : 'font-medium'}>{title}</span>}
|
||||
</p>
|
||||
{description && <p className="text-caption text-ink-3 mt-0.5 truncate-2">{description}</p>}
|
||||
{timestamp && <p className="text-caption text-ink-4 mt-1">{timestamp}</p>}
|
||||
</div>
|
||||
|
||||
{(status || trailing) && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{status && <StatusBadge status={status} size="sm" />}
|
||||
{trailing}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === 'card') {
|
||||
return (
|
||||
<Surface
|
||||
as={onClick ? 'button' : 'div'}
|
||||
onClick={onClick}
|
||||
interactive={onClick ? true : undefined}
|
||||
variant="solid"
|
||||
radius="lg"
|
||||
padding="sm"
|
||||
className={cn('flex items-start gap-3 w-full text-left', className)}
|
||||
>
|
||||
{body}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
const Comp = onClick ? 'button' : 'div';
|
||||
return (
|
||||
<Comp
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'flex items-start gap-3 w-full text-left rounded-xl p-2.5 transition-colors duration-fast',
|
||||
onClick && 'hover:bg-white/60 cursor-pointer focus-ring',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{body}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user