final annotation preview and export

This commit is contained in:
Siddharth
2025-12-01 11:20:05 -07:00
parent 6ac712eaac
commit 262745a97f
9 changed files with 663 additions and 400 deletions
@@ -1,27 +1,17 @@
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, Info, Shapes, Smile } 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';
import type { AnnotationRegion, AnnotationType, FigureType, ArrowDirection, ShapeType, FigureData } from "./types";
import type { AnnotationRegion, AnnotationType, ArrowDirection, FigureData } from "./types";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { cn } from "@/lib/utils";
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react';
import {
FaArrowUp, FaArrowDown, FaArrowLeft, FaArrowRight,
FaCircle, FaSquare, FaStar, FaHeart
} from "react-icons/fa";
import {
BsArrowUpRight, BsArrowDownRight, BsArrowDownLeft, BsArrowUpLeft
} from "react-icons/bs";
import { FaPlay } from "react-icons/fa";
import { BiRectangle } from "react-icons/bi";
import { getArrowComponent } from "./ArrowSvgs";
interface AnnotationSettingsPanelProps {
annotation: AnnotationRegion;
@@ -59,7 +49,6 @@ export function AnnotationSettingsPanel({
const [figureColorHsva, setFigureColorHsva] = useState(
hexToHsva(annotation.figureData?.color || '#34B27B')
);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@@ -121,8 +110,10 @@ export function AnnotationSettingsPanel({
Image
</TabsTrigger>
<TabsTrigger value="figure" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-lg transition-all gap-2">
<Shapes className="w-4 h-4" />
Figure
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 12h16m0 0l-6-6m6 6l-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Arrow
</TabsTrigger>
</TabsList>
@@ -365,240 +356,97 @@ export function AnnotationSettingsPanel({
<TabsContent value="figure" className="mt-0 space-y-4">
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">Figure Type</label>
<Tabs
value={annotation.figureData?.figureType || 'arrow'}
onValueChange={(value) => {
<label className="text-xs font-medium text-slate-200 mb-3 block">Arrow Direction</label>
<div className="grid grid-cols-4 gap-2">
{([
'up', 'down', 'left', 'right',
'up-right', 'up-left', 'down-right', 'down-left',
] as ArrowDirection[]).map((direction) => {
const ArrowComponent = getArrowComponent(direction);
return (
<button
key={direction}
onClick={() => {
const newFigureData: FigureData = {
...annotation.figureData!,
arrowDirection: direction,
};
onFigureDataChange?.(newFigureData);
}}
className={cn(
"h-16 rounded-lg border flex items-center justify-center transition-all p-2",
annotation.figureData?.arrowDirection === direction
? "bg-[#34B27B] border-[#34B27B]"
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"
)}
>
<ArrowComponent
color={annotation.figureData?.arrowDirection === direction ? "#ffffff" : "#94a3b8"}
strokeWidth={3}
/>
</button>
);
})}
</div>
</div>
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">
Stroke Width: {annotation.figureData?.strokeWidth || 4}px
</label>
<Slider
value={[annotation.figureData?.strokeWidth || 4]}
onValueChange={([value]) => {
const newFigureData: FigureData = {
...annotation.figureData!,
figureType: value as FigureType,
strokeWidth: value,
};
onFigureDataChange?.(newFigureData);
}}
min={1}
max={6}
step={1}
className="w-full"
>
<TabsList className="w-full grid grid-cols-3 bg-white/5 border border-white/5 p-1 h-auto rounded-lg">
<TabsTrigger value="arrow" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-md transition-all gap-1.5 text-xs">
<FaArrowRight className="w-3 h-3" />
Arrow
</TabsTrigger>
<TabsTrigger value="shape" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-md transition-all gap-1.5 text-xs">
<FaCircle className="w-3 h-3" />
Shape
</TabsTrigger>
<TabsTrigger value="emoji" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-slate-400 py-2 rounded-md transition-all gap-1.5 text-xs">
<Smile className="w-3 h-3" />
Emoji
</TabsTrigger>
</TabsList>
<TabsContent value="arrow" className="mt-4 space-y-4">
<div>
<label className="text-xs font-medium text-slate-200 mb-3 block">Arrows</label>
<div className="grid grid-cols-4 gap-2">
{[
{ value: 'up' as ArrowDirection, icon: FaArrowUp },
{ value: 'down' as ArrowDirection, icon: FaArrowDown },
{ value: 'left' as ArrowDirection, icon: FaArrowLeft },
{ value: 'right' as ArrowDirection, icon: FaArrowRight },
{ value: 'up-right' as ArrowDirection, icon: BsArrowUpRight },
{ value: 'up-left' as ArrowDirection, icon: BsArrowUpLeft },
{ value: 'down-right' as ArrowDirection, icon: BsArrowDownRight },
{ value: 'down-left' as ArrowDirection, icon: BsArrowDownLeft },
].map(({ value, icon: Icon }) => (
<button
key={value}
onClick={() => {
const newFigureData: FigureData = {
...annotation.figureData!,
arrowDirection: value,
};
onFigureDataChange?.(newFigureData);
}}
className={cn(
"h-16 rounded-lg border flex flex-col items-center justify-center gap-1 transition-all",
annotation.figureData?.arrowDirection === value
? "bg-[#34B27B] border-[#34B27B] text-white"
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10 hover:border-white/20"
)}
>
<Icon className="w-5 h-5" />
</button>
))}
</div>
</div>
</TabsContent>
<TabsContent value="shape" className="mt-4 space-y-4">
<div>
<label className="text-xs font-medium text-slate-200 mb-3 block">Shape Type</label>
<div className="grid grid-cols-3 gap-2">
{[
{ value: 'circle' as ShapeType, icon: FaCircle, label: 'Circle' },
{ value: 'square' as ShapeType, icon: FaSquare, label: 'Square' },
{ value: 'rectangle' as ShapeType, icon: BiRectangle, label: 'Rectangle' },
{ value: 'triangle' as ShapeType, icon: FaPlay, label: 'Triangle' },
{ value: 'star' as ShapeType, icon: FaStar, label: 'Star' },
{ value: 'heart' as ShapeType, icon: FaHeart, label: 'Heart' },
].map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => {
const newFigureData: FigureData = {
...annotation.figureData!,
shapeType: value,
};
onFigureDataChange?.(newFigureData);
}}
className={cn(
"h-16 rounded-lg border flex flex-col items-center justify-center gap-1 transition-all",
annotation.figureData?.shapeType === value
? "bg-[#34B27B] border-[#34B27B] text-white"
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10 hover:border-white/20"
)}
>
<Icon className="w-5 h-5" />
<span className="text-[10px]">{label}</span>
</button>
))}
</div>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg border border-white/5">
<label className="text-xs font-medium text-slate-200">Filled</label>
<Switch
checked={annotation.figureData?.filled ?? true}
onCheckedChange={(checked) => {
const newFigureData: FigureData = {
...annotation.figureData!,
filled: checked,
};
onFigureDataChange?.(newFigureData);
}}
/>
</div>
{!annotation.figureData?.filled && (
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">
Stroke Width: {annotation.figureData?.strokeWidth || 4}px
</label>
<Slider
value={[annotation.figureData?.strokeWidth || 4]}
onValueChange={([value]) => {
const newFigureData: FigureData = {
...annotation.figureData!,
strokeWidth: value,
};
onFigureDataChange?.(newFigureData);
}}
min={1}
max={20}
step={1}
className="w-full"
/>
</div>
)}
</TabsContent>
<TabsContent value="emoji" className="mt-4 space-y-4">
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">Selected Emoji</label>
<div className="flex items-center gap-3">
<div className="flex-1 h-16 bg-white/5 border border-white/10 rounded-lg flex items-center justify-center text-4xl">
{annotation.figureData?.emoji || '😊'}
</div>
<Button
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
variant="outline"
className="h-16 px-6 bg-[#34B27B] text-white border-[#34B27B] hover:bg-[#2a9163] hover:border-[#2a9163]"
>
{showEmojiPicker ? 'Close' : 'Pick'}
</Button>
</div>
</div>
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">
Emoji Size: {annotation.figureData?.emojiSize || 64}px
</label>
<Slider
value={[annotation.figureData?.emojiSize || 64]}
onValueChange={([value]) => {
const newFigureData: FigureData = {
...annotation.figureData!,
emojiSize: value,
};
onFigureDataChange?.(newFigureData);
}}
min={16}
max={200}
step={4}
className="w-full"
/>
</div>
{showEmojiPicker && (
<div className="border border-white/10 rounded-lg overflow-hidden">
<EmojiPicker
onEmojiClick={(emojiData: EmojiClickData) => {
const newFigureData: FigureData = {
...annotation.figureData!,
emoji: emojiData.emoji,
};
onFigureDataChange?.(newFigureData);
setShowEmojiPicker(false);
}}
width="100%"
height={300}
searchPlaceHolder="Search emoji..."
previewConfig={{ showPreview: false }}
/>
</div>
)}
</TabsContent>
</Tabs>
/>
</div>
{annotation.figureData?.figureType !== 'emoji' && (
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">Color</label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full h-10 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10"
>
<div
className="w-5 h-5 rounded-full border border-white/20"
style={{ backgroundColor: annotation.figureData?.color || '#34B27B' }}
/>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{annotation.figureData?.color || '#34B27B'}
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0 border-none bg-transparent shadow-xl">
<div className="p-2 bg-[#1a1a1c] border border-white/10 rounded-xl">
<Colorful
color={figureColorHsva}
disableAlpha={true}
onChange={(color) => {
setFigureColorHsva(color.hsva);
const newFigureData: FigureData = {
...annotation.figureData!,
color: hsvaToHex(color.hsva),
};
onFigureDataChange?.(newFigureData);
}}
style={{ width: '100%', borderRadius: '8px' }}
/>
</div>
</PopoverContent>
</Popover>
</div>
)}
<div>
<label className="text-xs font-medium text-slate-200 mb-2 block">Arrow Color</label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full h-10 justify-start gap-2 bg-white/5 border-white/10 hover:bg-white/10"
>
<div
className="w-5 h-5 rounded-full border border-white/20"
style={{ backgroundColor: annotation.figureData?.color || '#34B27B' }}
/>
<span className="text-xs text-slate-300 truncate flex-1 text-left">
{annotation.figureData?.color || '#34B27B'}
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0 border-none bg-transparent shadow-xl">
<div className="p-2 bg-[#1a1a1c] border border-white/10 rounded-xl">
<Colorful
color={figureColorHsva}
disableAlpha={true}
onChange={(color) => {
setFigureColorHsva(color.hsva);
const newFigureData: FigureData = {
...annotation.figureData!,
color: hsvaToHex(color.hsva),
};
onFigureDataChange?.(newFigureData);
}}
style={{ width: '100%', borderRadius: '8px' }}
/>
</div>
</PopoverContent>
</Popover>
</div>
</TabsContent>
</Tabs>