100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
import * as React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { StatusBadge } from '@/components/ds/StatusBadge';
|
|
|
|
const TONE_DOT = {
|
|
brand: 'bg-krow-blue ring-krow-blue/20',
|
|
success: 'bg-success ring-success/20',
|
|
warning: 'bg-warning ring-warning/20',
|
|
destructive: 'bg-destructive ring-destructive/20',
|
|
neutral: 'bg-ink-4 ring-ink-4/20',
|
|
};
|
|
|
|
/**
|
|
* Timeline — a vertical sequence of events: a candidate's journey, an audit
|
|
* trail, a career history.
|
|
*
|
|
* Items:
|
|
* { id, title, description, timestamp, icon, tone, status, meta, current }
|
|
*
|
|
* The connecting rail is drawn per item rather than as one absolute line, so a
|
|
* timeline can be nested inside a scrolling card without the rail detaching.
|
|
*/
|
|
export function Timeline({ items = [], className, compact = false }) {
|
|
return (
|
|
<ol className={cn('relative', className)}>
|
|
{items.map((item, i) => {
|
|
const isLast = i === items.length - 1;
|
|
const Icon = item.icon;
|
|
const tone = item.tone || (item.current ? 'brand' : 'neutral');
|
|
|
|
return (
|
|
<li key={item.id ?? i} className="relative flex gap-3.5 pb-5 last:pb-0">
|
|
{/* Rail + node */}
|
|
<div className="relative flex flex-col items-center shrink-0">
|
|
{Icon ? (
|
|
<span
|
|
className={cn(
|
|
'grid place-items-center rounded-full ring-4 shrink-0 z-10',
|
|
compact ? 'w-6 h-6' : 'w-8 h-8',
|
|
tone === 'neutral' ? 'bg-surface-sunken text-ink-3 ring-white' : 'text-white ring-white',
|
|
tone !== 'neutral' && TONE_DOT[tone]
|
|
)}
|
|
>
|
|
<Icon className={compact ? 'w-3 h-3' : 'w-4 h-4'} aria-hidden="true" />
|
|
</span>
|
|
) : (
|
|
<span
|
|
className={cn(
|
|
'rounded-full ring-4 ring-white shrink-0 z-10 mt-1',
|
|
compact ? 'w-2 h-2' : 'w-2.5 h-2.5',
|
|
TONE_DOT[tone],
|
|
item.current && 'animate-pulse-ring'
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
|
|
{!isLast && (
|
|
<span
|
|
aria-hidden="true"
|
|
className="w-px flex-1 bg-border mt-1.5 min-h-4"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className={cn('flex-1 min-w-0', compact ? '-mt-0.5' : '-mt-1')}>
|
|
<div className="flex items-start justify-between gap-2 flex-wrap">
|
|
<div className="min-w-0">
|
|
<p
|
|
className={cn(
|
|
'font-medium text-ink-1',
|
|
compact ? 'text-body-sm' : 'text-body'
|
|
)}
|
|
>
|
|
{item.title}
|
|
</p>
|
|
{item.description && (
|
|
<p className="text-body-sm text-ink-3 mt-0.5 leading-relaxed">{item.description}</p>
|
|
)}
|
|
{item.meta && <p className="text-caption text-ink-4 mt-1">{item.meta}</p>}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
{item.status && <StatusBadge status={item.status} size="sm" />}
|
|
{item.timestamp && (
|
|
<time className="text-caption text-ink-4 whitespace-nowrap">{item.timestamp}</time>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{item.children}
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
);
|
|
}
|