217 lines
7.5 KiB
TypeScript
217 lines
7.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import * as React from 'react';
|
||
|
|
import { cn } from '@/lib/utils/cn';
|
||
|
|
import type { Marker } from '@/types/marker';
|
||
|
|
import { Flag, Edit2, Trash2 } from 'lucide-react';
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
|
||
|
|
export interface MarkerTimelineProps {
|
||
|
|
markers: Marker[];
|
||
|
|
duration: number;
|
||
|
|
currentTime: number;
|
||
|
|
onMarkerClick?: (marker: Marker) => void;
|
||
|
|
onMarkerEdit?: (marker: Marker) => void;
|
||
|
|
onMarkerDelete?: (markerId: string) => void;
|
||
|
|
onSeek?: (time: number) => void;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function MarkerTimeline({
|
||
|
|
markers,
|
||
|
|
duration,
|
||
|
|
currentTime,
|
||
|
|
onMarkerClick,
|
||
|
|
onMarkerEdit,
|
||
|
|
onMarkerDelete,
|
||
|
|
onSeek,
|
||
|
|
className,
|
||
|
|
}: MarkerTimelineProps) {
|
||
|
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||
|
|
const [hoveredMarkerId, setHoveredMarkerId] = React.useState<string | null>(null);
|
||
|
|
|
||
|
|
const timeToX = React.useCallback(
|
||
|
|
(time: number): number => {
|
||
|
|
if (!containerRef.current) return 0;
|
||
|
|
const width = containerRef.current.clientWidth;
|
||
|
|
return (time / duration) * width;
|
||
|
|
},
|
||
|
|
[duration]
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
ref={containerRef}
|
||
|
|
className={cn(
|
||
|
|
'relative w-full h-8 bg-muted/30 border-b border-border',
|
||
|
|
className
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{/* Markers */}
|
||
|
|
{markers.map((marker) => {
|
||
|
|
const x = timeToX(marker.time);
|
||
|
|
const isHovered = hoveredMarkerId === marker.id;
|
||
|
|
|
||
|
|
if (marker.type === 'point') {
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={marker.id}
|
||
|
|
className="absolute top-0 bottom-0 group cursor-pointer"
|
||
|
|
style={{ left: `${x}px` }}
|
||
|
|
onMouseEnter={() => setHoveredMarkerId(marker.id)}
|
||
|
|
onMouseLeave={() => setHoveredMarkerId(null)}
|
||
|
|
onClick={() => {
|
||
|
|
onMarkerClick?.(marker);
|
||
|
|
onSeek?.(marker.time);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* Marker line */}
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'absolute top-0 bottom-0 w-0.5 transition-colors',
|
||
|
|
isHovered ? 'bg-primary' : 'bg-primary/60'
|
||
|
|
)}
|
||
|
|
style={{ backgroundColor: marker.color }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Marker flag */}
|
||
|
|
<Flag
|
||
|
|
className={cn(
|
||
|
|
'absolute top-0.5 -left-2 h-4 w-4 transition-colors',
|
||
|
|
isHovered ? 'text-primary' : 'text-primary/60'
|
||
|
|
)}
|
||
|
|
style={{ color: marker.color }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Hover tooltip with actions */}
|
||
|
|
{isHovered && (
|
||
|
|
<div className="absolute top-full left-0 mt-1 z-10 bg-popover border border-border rounded shadow-lg p-2 min-w-[200px]">
|
||
|
|
<div className="text-xs font-medium mb-1">{marker.name}</div>
|
||
|
|
{marker.description && (
|
||
|
|
<div className="text-xs text-muted-foreground mb-2">{marker.description}</div>
|
||
|
|
)}
|
||
|
|
<div className="flex gap-1">
|
||
|
|
{onMarkerEdit && (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon-sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
onMarkerEdit(marker);
|
||
|
|
}}
|
||
|
|
title="Edit marker"
|
||
|
|
className="h-6 w-6"
|
||
|
|
>
|
||
|
|
<Edit2 className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{onMarkerDelete && (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon-sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
onMarkerDelete(marker.id);
|
||
|
|
}}
|
||
|
|
title="Delete marker"
|
||
|
|
className="h-6 w-6 text-destructive hover:text-destructive"
|
||
|
|
>
|
||
|
|
<Trash2 className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
// Region marker
|
||
|
|
const endX = timeToX(marker.endTime || marker.time);
|
||
|
|
const width = endX - x;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={marker.id}
|
||
|
|
className="absolute top-0 bottom-0 group cursor-pointer"
|
||
|
|
style={{ left: `${x}px`, width: `${width}px` }}
|
||
|
|
onMouseEnter={() => setHoveredMarkerId(marker.id)}
|
||
|
|
onMouseLeave={() => setHoveredMarkerId(null)}
|
||
|
|
onClick={() => {
|
||
|
|
onMarkerClick?.(marker);
|
||
|
|
onSeek?.(marker.time);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* Region background */}
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'absolute inset-0 transition-opacity',
|
||
|
|
isHovered ? 'opacity-30' : 'opacity-20'
|
||
|
|
)}
|
||
|
|
style={{ backgroundColor: marker.color || 'var(--color-primary)' }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Region borders */}
|
||
|
|
<div
|
||
|
|
className="absolute top-0 bottom-0 left-0 w-0.5"
|
||
|
|
style={{ backgroundColor: marker.color || 'var(--color-primary)' }}
|
||
|
|
/>
|
||
|
|
<div
|
||
|
|
className="absolute top-0 bottom-0 right-0 w-0.5"
|
||
|
|
style={{ backgroundColor: marker.color || 'var(--color-primary)' }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* Region label */}
|
||
|
|
<div
|
||
|
|
className="absolute top-0.5 left-1 text-[10px] font-medium truncate pr-1"
|
||
|
|
style={{ color: marker.color || 'var(--color-primary)', maxWidth: `${width - 8}px` }}
|
||
|
|
>
|
||
|
|
{marker.name}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Hover tooltip with actions */}
|
||
|
|
{isHovered && (
|
||
|
|
<div className="absolute top-full left-0 mt-1 z-10 bg-popover border border-border rounded shadow-lg p-2 min-w-[200px]">
|
||
|
|
<div className="text-xs font-medium mb-1">{marker.name}</div>
|
||
|
|
{marker.description && (
|
||
|
|
<div className="text-xs text-muted-foreground mb-2">{marker.description}</div>
|
||
|
|
)}
|
||
|
|
<div className="flex gap-1">
|
||
|
|
{onMarkerEdit && (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon-sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
onMarkerEdit(marker);
|
||
|
|
}}
|
||
|
|
title="Edit marker"
|
||
|
|
className="h-6 w-6"
|
||
|
|
>
|
||
|
|
<Edit2 className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
{onMarkerDelete && (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="icon-sm"
|
||
|
|
onClick={(e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
onMarkerDelete(marker.id);
|
||
|
|
}}
|
||
|
|
title="Delete marker"
|
||
|
|
className="h-6 w-6 text-destructive hover:text-destructive"
|
||
|
|
>
|
||
|
|
<Trash2 className="h-3 w-3" />
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|