feat(ticket): redesign audit timeline with structured history events
This commit is contained in:
115
src/components/ui/timeline.tsx
Normal file
115
src/components/ui/timeline.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Timeline({ className, ...props }: React.ComponentProps<'ol'>) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="timeline"
|
||||
className={cn('flex w-full flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TimelineItem({
|
||||
className,
|
||||
marker,
|
||||
opposite,
|
||||
layout = 'alternate',
|
||||
side = 'right',
|
||||
isLast = false,
|
||||
children,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<'li'>, 'children'> & {
|
||||
marker: React.ReactNode;
|
||||
opposite: React.ReactNode;
|
||||
layout?: 'left' | 'alternate';
|
||||
side?: 'left' | 'right';
|
||||
isLast?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (layout === 'left') {
|
||||
return (
|
||||
<li
|
||||
data-slot="timeline-item"
|
||||
className={cn(
|
||||
'grid grid-cols-[minmax(5.5rem,auto)_1rem_minmax(0,1fr)] items-start gap-x-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-slot="timeline-opposite"
|
||||
className="pt-0.5 text-right tabular-nums"
|
||||
>
|
||||
{opposite}
|
||||
</div>
|
||||
|
||||
<div className="relative flex justify-center self-stretch">
|
||||
{!isLast ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute top-4 bottom-0 w-px bg-border"
|
||||
/>
|
||||
) : null}
|
||||
<div data-slot="timeline-marker" className="relative z-10">
|
||||
{marker}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-slot="timeline-content"
|
||||
className={cn('min-w-0 pb-8', isLast && 'pb-0')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
data-slot="timeline-item"
|
||||
className={cn(
|
||||
'grid grid-cols-[2.5rem_minmax(0,1fr)] grid-rows-[auto_auto] gap-x-3 md:grid-cols-[minmax(0,1fr)_2.5rem_minmax(0,1fr)] md:grid-rows-1 md:gap-x-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-slot="timeline-opposite"
|
||||
className={cn(
|
||||
'col-start-2 row-start-1 mb-2 pt-0.5 md:mb-0',
|
||||
side === 'right'
|
||||
? 'md:col-start-1 md:text-right'
|
||||
: 'md:col-start-3 md:text-left',
|
||||
)}
|
||||
>
|
||||
{opposite}
|
||||
</div>
|
||||
<div className="relative col-start-1 row-span-2 row-start-1 flex justify-center md:col-start-2 md:row-span-1">
|
||||
{!isLast ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute top-4 bottom-0 w-px bg-border"
|
||||
/>
|
||||
) : null}
|
||||
<div data-slot="timeline-marker" className="relative z-10">
|
||||
{marker}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-slot="timeline-content"
|
||||
className={cn(
|
||||
'col-start-2 row-start-2 min-w-0 pb-7 md:row-start-1 md:pb-8',
|
||||
side === 'right' ? 'md:col-start-3' : 'md:col-start-1',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export { Timeline, TimelineItem };
|
||||
Reference in New Issue
Block a user