improved annotation experience

This commit is contained in:
Siddharth
2025-11-30 19:19:08 -07:00
parent c847953a52
commit 79e40cef68
6 changed files with 52 additions and 7 deletions
@@ -1,3 +1,4 @@
import { useRef } from "react";
import { Rnd } from "react-rnd";
import type { AnnotationRegion } from "./types";
import { cn } from "@/lib/utils";
@@ -42,6 +43,8 @@ export function AnnotationOverlay({
isSelected
});
const isDraggingRef = useRef(false);
const renderContent = () => {
switch (annotation.type) {
case 'text':
@@ -107,10 +110,18 @@ export function AnnotationOverlay({
<Rnd
position={{ x, y }}
size={{ width, height }}
onDragStart={() => {
isDraggingRef.current = true;
}}
onDragStop={(_e, d) => {
const xPercent = (d.x / containerWidth) * 100;
const yPercent = (d.y / containerHeight) * 100;
onPositionChange(annotation.id, { x: xPercent, y: yPercent });
// Reset dragging flag after a short delay to prevent click event
setTimeout(() => {
isDraggingRef.current = false;
}, 100);
}}
onResizeStop={(_e, _direction, ref, _delta, position) => {
const xPercent = (position.x / containerWidth) * 100;
@@ -120,7 +131,10 @@ export function AnnotationOverlay({
onPositionChange(annotation.id, { x: xPercent, y: yPercent });
onSizeChange(annotation.id, { width: widthPercent, height: heightPercent });
}}
onClick={() => onClick(annotation.id)}
onClick={() => {
if (isDraggingRef.current) return;
onClick(annotation.id);
}}
bounds="parent"
className={cn(
"cursor-move transition-all",
@@ -1,7 +1,7 @@
import { useState, useRef } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown } from "lucide-react";
import { Trash2, Type, Image as ImageIcon, Upload, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, ChevronDown, Info } from "lucide-react";
import { toast } from "sonner";
import Colorful from '@uiw/react-color-colorful';
import { hsvaToHex, hexToHsva } from '@uiw/color-convert';
@@ -351,6 +351,18 @@ export function AnnotationSettingsPanel({
<Trash2 className="w-4 h-4" />
Delete Annotation
</Button>
<div className="mt-6 p-3 bg-white/5 rounded-lg border border-white/5">
<div className="flex items-center gap-2 mb-2 text-slate-300">
<Info className="w-3.5 h-3.5" />
<span className="text-xs font-medium">Shortcuts & Tips</span>
</div>
<ul className="text-[10px] text-slate-400 space-y-1.5 list-disc pl-3 leading-relaxed">
<li>Move playhead to overlapping annotation section and select an item.</li>
<li>Use <kbd className="px-1 py-0.5 bg-white/10 rounded text-slate-300 font-mono">Tab</kbd> to cycle through overlapping items.</li>
<li>Use <kbd className="px-1 py-0.5 bg-white/10 rounded text-slate-300 font-mono">Shift+Tab</kbd> to cycle backwards.</li>
</ul>
</div>
</div>
</div>
);
@@ -348,6 +348,22 @@ export default function VideoEditor() {
),
);
}, []);
// Global Tab prevention
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
// Allow tab only in inputs/textareas
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
e.preventDefault();
}
};
window.addEventListener('keydown', handleKeyDown, { capture: true });
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
}, []);
useEffect(() => {
if (selectedZoomId && !zoomRegions.some((region) => region.id === selectedZoomId)) {
@@ -800,6 +800,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
{(() => {
const filtered = (annotationRegions || []).filter((annotation) => {
if (typeof annotation.startMs !== 'number' || typeof annotation.endMs !== 'number') return false;
if (annotation.id === selectedAnnotationId) return true;
const timeMs = Math.round(currentTime * 1000);
return timeMs >= annotation.startMs && timeMs <= annotation.endMs;
});
@@ -32,7 +32,8 @@ export default function Item({
isSelected = false,
onSelect,
zoomDepth = 1,
variant = 'zoom'
variant = 'zoom',
children
}: ItemProps) {
const { setNodeRef, attributes, listeners, itemStyle, itemContentStyle } = useItem({
id,
@@ -107,7 +108,7 @@ export default function Item({
<>
<MessageSquare className="w-3.5 h-3.5" />
<span className="text-[11px] font-semibold tracking-tight">
Note
{children}
</span>
</>
)}
@@ -688,8 +688,7 @@ export default function TimelineEditor({
onSelectAnnotation?.(overlapping[nextIndex].id);
}
}
}
}
if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) {
if (selectedKeyframeId) {
deleteSelectedKeyframe();
@@ -743,7 +742,7 @@ export default function TimelineEditor({
const preview = region.content.trim() || 'Empty text';
label = preview.length > 20 ? `${preview.substring(0, 20)}...` : preview;
} else if (region.type === 'image') {
label = '🖼️ Image';
label = 'Image';
} else {
label = 'Annotation';
}