0a430e5c60
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
167 lines
6.6 KiB
TypeScript
167 lines
6.6 KiB
TypeScript
import { Eye, EyeOff, Lock, Unlock, Copy, Trash2, Box, Layers, Check } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
import { useModelStore } from '@/stores/useModelStore';
|
|
import { toast } from 'sonner';
|
|
|
|
const COLOR_PALETTE = [
|
|
'#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e',
|
|
'#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1',
|
|
'#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#64748b',
|
|
'#ffffff', '#1f2937',
|
|
];
|
|
|
|
interface SceneModelListProps {
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function SceneModelList({ compact = false }: SceneModelListProps) {
|
|
const models = useModelStore((s) => s.models);
|
|
const activeId = useModelStore((s) => s.activeModelId);
|
|
const setActive = useModelStore((s) => s.setActiveModel);
|
|
const removeModel = useModelStore((s) => s.removeModel);
|
|
const toggleVisible = useModelStore((s) => s.toggleModelVisible);
|
|
const toggleLocked = useModelStore((s) => s.toggleModelLocked);
|
|
const duplicate = useModelStore((s) => s.duplicateModel);
|
|
const setModelColor = useModelStore((s) => s.setModelColor);
|
|
const max = useModelStore((s) => s.maxModels);
|
|
|
|
if (models.length === 0) {
|
|
return (
|
|
<div className="rounded-lg border border-dashed bg-muted/20 p-3 text-center">
|
|
<Layers className="mx-auto mb-1 h-4 w-4 text-muted-foreground" />
|
|
<p className="font-mono text-[10px] text-muted-foreground">Nenhuma peça na cena</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const textSize = compact ? 'text-[9px]' : 'text-[11px]';
|
|
const padding = compact ? 'p-1.5' : 'p-2';
|
|
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-1.5">
|
|
<Layers className="h-3 w-3 text-primary" />
|
|
<span className={`font-mono ${textSize} font-semibold uppercase tracking-wider text-muted-foreground`}>
|
|
Cena
|
|
</span>
|
|
</div>
|
|
<span className={`font-mono ${textSize} text-muted-foreground`}>
|
|
{models.length}/{max}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
{models.map((m) => {
|
|
const isActive = m.id === activeId;
|
|
return (
|
|
<div
|
|
key={m.id}
|
|
className={`flex items-center gap-1 rounded-md border ${padding} transition-colors cursor-pointer ${
|
|
isActive
|
|
? 'border-primary bg-primary/10'
|
|
: 'border-border bg-muted/30 hover:bg-muted/50'
|
|
}`}
|
|
onClick={() => setActive(m.id)}
|
|
>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="h-2.5 w-2.5 shrink-0 rounded-full ring-1 ring-foreground/20 hover:ring-2 hover:ring-primary cursor-pointer"
|
|
style={{ backgroundColor: m.color }}
|
|
title="Trocar cor da peça"
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="w-auto p-2"
|
|
side="right"
|
|
align="start"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<p className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground mb-2">
|
|
Cor da peça
|
|
</p>
|
|
<div className="grid grid-cols-5 gap-1.5">
|
|
{COLOR_PALETTE.map((c) => {
|
|
const selected = c.toLowerCase() === m.color.toLowerCase();
|
|
return (
|
|
<button
|
|
key={c}
|
|
type="button"
|
|
className="h-6 w-6 rounded-md ring-1 ring-foreground/20 hover:scale-110 transition-transform flex items-center justify-center"
|
|
style={{ backgroundColor: c }}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setModelColor(m.id, c);
|
|
}}
|
|
title={c}
|
|
>
|
|
{selected && <Check className="h-3 w-3" style={{ color: c === '#ffffff' ? '#000' : '#fff' }} />}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<Box className={`h-3 w-3 shrink-0 ${isActive ? 'text-primary' : 'text-muted-foreground'}`} />
|
|
<span className={`flex-1 truncate font-mono ${textSize} ${isActive ? 'text-foreground font-semibold' : 'text-muted-foreground'}`}>
|
|
{m.fileName}
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 shrink-0"
|
|
onClick={(e) => { e.stopPropagation(); toggleVisible(m.id); }}
|
|
title={m.visible ? 'Ocultar' : 'Exibir'}
|
|
>
|
|
{m.visible ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3 text-muted-foreground" />}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 shrink-0"
|
|
onClick={(e) => { e.stopPropagation(); toggleLocked(m.id); }}
|
|
title={m.locked ? 'Destravar' : 'Travar'}
|
|
>
|
|
{m.locked ? <Lock className="h-3 w-3 text-warning" /> : <Unlock className="h-3 w-3 text-muted-foreground" />}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 shrink-0"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (models.length >= max) {
|
|
toast.error(`Limite de ${max} peças atingido`);
|
|
return;
|
|
}
|
|
duplicate(m.id);
|
|
}}
|
|
title="Duplicar"
|
|
>
|
|
<Copy className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 shrink-0"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
removeModel(m.id);
|
|
toast.success(`"${m.fileName}" removido`);
|
|
}}
|
|
title="Remover"
|
|
>
|
|
<Trash2 className="h-3 w-3 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|